@managemint-solutions/sdk 0.36.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.
@@ -0,0 +1,29 @@
1
+ "use strict";
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 resources hang off
4
+ // `SupabaseClient` as `leaveTypes`, `leaveEntitlements` and `leaveRequests`; the holiday
5
+ // calendar the day count reads is its own resource, `publicHolidays` (src/public-holidays).
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.LeaveRequestsResource = exports.LeaveEntitlementsResource = exports.LeaveTypesResource = void 0;
22
+ __exportStar(require("./dto"), exports);
23
+ __exportStar(require("./working-days"), exports);
24
+ var leave_types_1 = require("./leave-types");
25
+ Object.defineProperty(exports, "LeaveTypesResource", { enumerable: true, get: function () { return leave_types_1.LeaveTypesResource; } });
26
+ var leave_entitlements_1 = require("./leave-entitlements");
27
+ Object.defineProperty(exports, "LeaveEntitlementsResource", { enumerable: true, get: function () { return leave_entitlements_1.LeaveEntitlementsResource; } });
28
+ var leave_requests_1 = require("./leave-requests");
29
+ Object.defineProperty(exports, "LeaveRequestsResource", { enumerable: true, get: function () { return leave_requests_1.LeaveRequestsResource; } });
@@ -0,0 +1,42 @@
1
+ import type { MmsList } from '@managemint-solutions/entities/common';
2
+ import type { LeaveEntitlementEntity, LeavePeriod } from '@managemint-solutions/entities/leave';
3
+ import type { AuthContext } from '../auth';
4
+ import { type AuditResource } from '../audit';
5
+ import type { TypedSupabaseClient } from '../client';
6
+ import type { GetLeaveEntitlementsDto, UpdateLeaveEntitlementDto } from './dto';
7
+ /** What the request path needs of the entitlement covering a date: the period and the cap. */
8
+ export type LeaveEntitlementCover = LeavePeriod & {
9
+ mms_id: string;
10
+ days_entitled: number;
11
+ };
12
+ /** The create input after the api has resolved the period defaults. */
13
+ export type CreateLeaveEntitlementInput = LeavePeriod & {
14
+ user_id: string;
15
+ leave_type_id: string;
16
+ days_entitled: number;
17
+ notes?: string | null;
18
+ };
19
+ export declare class LeaveEntitlementsResource {
20
+ private readonly supabase;
21
+ private readonly auth;
22
+ private readonly table;
23
+ constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
24
+ list(query: GetLeaveEntitlementsDto): Promise<MmsList<LeaveEntitlementEntity>>;
25
+ getById(leaveEntitlementId: string): Promise<LeaveEntitlementEntity>;
26
+ /**
27
+ * The entitlement of `userId` for `leaveTypeId` whose period contains `date`, or null when the
28
+ * type is not tracked for them then. The periods of one user and type cannot overlap (the
29
+ * table's exclusion constraint), so there is at most one.
30
+ */
31
+ findFor(userId: string, leaveTypeId: string, date: string): Promise<LeaveEntitlementCover | null>;
32
+ create(input: CreateLeaveEntitlementInput): Promise<LeaveEntitlementEntity>;
33
+ update(leaveEntitlementId: string, input: UpdateLeaveEntitlementDto): Promise<LeaveEntitlementEntity>;
34
+ /** Hard delete: the type simply becomes untracked for that user and period. */
35
+ remove(leaveEntitlementId: string): Promise<void>;
36
+ /**
37
+ * Adds the computed balance to each row from one query over the requests that could count
38
+ * against any of them - the approved and pending ones of these users and types, starting in
39
+ * the span the periods cover. Nothing is stored: a balance is always what the requests say.
40
+ */
41
+ private withBalances;
42
+ }
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LeaveEntitlementsResource = 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_ENTITLEMENT_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
+ period_start,
21
+ period_end,
22
+ days_entitled,
23
+ notes
24
+ `;
25
+ class LeaveEntitlementsResource {
26
+ supabase;
27
+ auth;
28
+ table;
29
+ constructor(supabase, auth, audit) {
30
+ this.supabase = supabase;
31
+ this.auth = auth;
32
+ this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
33
+ table: 'leave_entitlements',
34
+ entityType: enum_1.AuditTrailEntityType.LEAVE_ENTITLEMENTS,
35
+ });
36
+ }
37
+ async list(query) {
38
+ const page = query.page;
39
+ const pageSize = query.page_size;
40
+ const offset = (page - 1) * pageSize;
41
+ let entitlementsQuery = this.supabase
42
+ .from('leave_entitlements')
43
+ .select(LEAVE_ENTITLEMENT_SELECT, { count: 'estimated' })
44
+ .eq('organization_id', this.auth.organization_id)
45
+ .order('period_start', { ascending: false })
46
+ .order('created_at', { ascending: false })
47
+ .range(offset, offset + pageSize - 1);
48
+ if (query.user_id) {
49
+ entitlementsQuery = entitlementsQuery.eq('user_id', query.user_id);
50
+ }
51
+ if (query.leave_type_id) {
52
+ entitlementsQuery = entitlementsQuery.eq('leave_type_id', query.leave_type_id);
53
+ }
54
+ if (query.as_of) {
55
+ entitlementsQuery = entitlementsQuery.lte('period_start', query.as_of).gte('period_end', query.as_of);
56
+ }
57
+ const { data, error, count } = await entitlementsQuery;
58
+ if (error)
59
+ throw (0, errors_1.mapPostgrestError)(error);
60
+ const rows = await this.withBalances((data ?? []));
61
+ return (0, transforms_1.toMmsList)(rows, page, pageSize, count);
62
+ }
63
+ async getById(leaveEntitlementId) {
64
+ const { data, error } = await this.supabase
65
+ .from('leave_entitlements')
66
+ .select(LEAVE_ENTITLEMENT_SELECT)
67
+ .eq('mms_id', leaveEntitlementId)
68
+ .eq('organization_id', this.auth.organization_id)
69
+ .single();
70
+ if (error)
71
+ throw (0, errors_1.mapPostgrestError)(error);
72
+ const [row] = await this.withBalances([data]);
73
+ return row;
74
+ }
75
+ /**
76
+ * The entitlement of `userId` for `leaveTypeId` whose period contains `date`, or null when the
77
+ * type is not tracked for them then. The periods of one user and type cannot overlap (the
78
+ * table's exclusion constraint), so there is at most one.
79
+ */
80
+ async findFor(userId, leaveTypeId, date) {
81
+ const { data, error } = await this.supabase
82
+ .from('leave_entitlements')
83
+ .select('mms_id, period_start, period_end, days_entitled')
84
+ .eq('organization_id', this.auth.organization_id)
85
+ .eq('user_id', userId)
86
+ .eq('leave_type_id', leaveTypeId)
87
+ .lte('period_start', date)
88
+ .gte('period_end', date)
89
+ .maybeSingle();
90
+ if (error)
91
+ throw (0, errors_1.mapPostgrestError)(error);
92
+ if (!data)
93
+ return null;
94
+ const cover = data;
95
+ return { ...cover, days_entitled: Number(cover.days_entitled) };
96
+ }
97
+ async create(input) {
98
+ const row = await this.table.insert({
99
+ user_id: input.user_id,
100
+ leave_type_id: input.leave_type_id,
101
+ period_start: input.period_start,
102
+ period_end: input.period_end,
103
+ days_entitled: input.days_entitled,
104
+ notes: input.notes ?? null,
105
+ organization_id: this.auth.organization_id,
106
+ created_by: this.auth.mms_id,
107
+ created_at: new Date().toISOString(),
108
+ });
109
+ return this.getById(row.mms_id);
110
+ }
111
+ async update(leaveEntitlementId, input) {
112
+ await this.table.update(leaveEntitlementId, {
113
+ days_entitled: input.days_entitled,
114
+ period_end: input.period_end,
115
+ notes: input.notes,
116
+ last_updated_by: this.auth.mms_id,
117
+ updated_at: new Date().toISOString(),
118
+ });
119
+ return this.getById(leaveEntitlementId);
120
+ }
121
+ /** Hard delete: the type simply becomes untracked for that user and period. */
122
+ async remove(leaveEntitlementId) {
123
+ await this.table.remove(leaveEntitlementId);
124
+ }
125
+ /**
126
+ * Adds the computed balance to each row from one query over the requests that could count
127
+ * against any of them - the approved and pending ones of these users and types, starting in
128
+ * the span the periods cover. Nothing is stored: a balance is always what the requests say.
129
+ */
130
+ async withBalances(rows) {
131
+ if (rows.length === 0)
132
+ return [];
133
+ const userIds = [...new Set(rows.map((row) => row.user.mms_id))];
134
+ const typeIds = [...new Set(rows.map((row) => row.leave_type.mms_id))];
135
+ const earliest = rows.map((row) => row.period_start).sort()[0];
136
+ const latest = rows.map((row) => row.period_end).sort().reverse()[0];
137
+ const { data, error } = await this.supabase
138
+ .from('leave_requests')
139
+ .select('user_id, leave_type_id, start_date, days, status')
140
+ .eq('organization_id', this.auth.organization_id)
141
+ .in('user_id', userIds)
142
+ .in('leave_type_id', typeIds)
143
+ .in('status', [enum_2.LeaveRequestStatus.APPROVED, enum_2.LeaveRequestStatus.PENDING])
144
+ .gte('start_date', earliest)
145
+ .lte('start_date', latest);
146
+ if (error)
147
+ throw (0, errors_1.mapPostgrestError)(error);
148
+ const requests = (data ?? []);
149
+ return rows.map((row) => {
150
+ const mine = requests.filter((request) => request.user_id === row.user.mms_id &&
151
+ request.leave_type_id === row.leave_type.mms_id &&
152
+ request.start_date >= row.period_start &&
153
+ request.start_date <= row.period_end);
154
+ const sum = (status) => mine
155
+ .filter((request) => request.status === status)
156
+ .reduce((total, request) => total + Number(request.days), 0);
157
+ const daysEntitled = Number(row.days_entitled);
158
+ const daysUsed = sum(enum_2.LeaveRequestStatus.APPROVED);
159
+ const daysPending = sum(enum_2.LeaveRequestStatus.PENDING);
160
+ return {
161
+ ...row,
162
+ days_entitled: daysEntitled,
163
+ days_used: daysUsed,
164
+ days_pending: daysPending,
165
+ days_remaining: daysEntitled - daysUsed - daysPending,
166
+ };
167
+ });
168
+ }
169
+ }
170
+ exports.LeaveEntitlementsResource = LeaveEntitlementsResource;
@@ -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;