@managemint-solutions/sdk 0.45.0 → 0.49.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.
@@ -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
- requires_document
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
- * The days of `userId`'s requests of `leaveTypeId` in `statuses` that start inside `period` -
115
- * what an entitlement's balance is checked against. `excludeId` leaves out the request being
116
- * edited or approved so it is not counted against itself.
117
- */
118
- async sumDays(userId, leaveTypeId, period, statuses, excludeId) {
119
- let daysQuery = this.supabase
120
- .from('leave_requests')
121
- .select('days')
122
- .eq('organization_id', this.auth.organization_id)
123
- .eq('user_id', userId)
124
- .eq('leave_type_id', leaveTypeId)
125
- .in('status', statuses)
126
- .gte('start_date', period.period_start)
127
- .lte('start_date', period.period_end);
128
- if (excludeId) {
129
- daysQuery = daysQuery.neq('mms_id', excludeId);
130
- }
131
- const { data, error } = await daysQuery;
132
- if (error)
133
- throw (0, errors_1.mapPostgrestError)(error);
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
- /** Cancels a request while it is still pending; null when it no longer is - a decided request is final. */
173
- async cancel(leaveRequestId, cancelledBy) {
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', enum_2.LeaveRequestStatus.PENDING), {
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
- requires_document,
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
- requires_document: input.requires_document,
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
- requires_document: input.requires_document,
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
- /** Monday to Friday and not one of the organization's holidays. */
16
- export declare const isWorkingDay: (isoDate: string, holidays: ReadonlySet<string>) => boolean;
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 weekend or holiday - the caller rejects a zero either way.
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
- * The leave year containing `date` for an organization whose year starts on the first of
29
- * `startMonth` (1-12, `organizations.leave_calendar_start_month`). A March start puts
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 leavePeriodFor: (date: string, startMonth: number) => LeavePeriod;
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.leavePeriodFor = exports.addOneYearMinusDay = exports.shiftDays = exports.countWorkingDays = exports.isWorkingDay = exports.eachDate = exports.todayInJohannesburg = exports.toIsoDate = exports.parseIsoDate = 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
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
- /** Saturday and Sunday. Not configurable yet - a working-week setting is a later feature. */
13
- const WEEKEND_DAYS = new Set([0, 6]);
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
- /** Monday to Friday and not one of the organization's holidays. */
45
- const isWorkingDay = (isoDate, holidays) => !WEEKEND_DAYS.has((0, exports.parseIsoDate)(isoDate).getUTCDay()) && !holidays.has(isoDate.slice(0, 10));
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 weekend or holiday - the caller rejects a zero either way.
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
- /** The day before the same date next year - the end of a period that starts on `isoDate`. */
65
- const addOneYearMinusDay = (isoDate) => {
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.setUTCFullYear(date.getUTCFullYear() + 1);
68
- date.setUTCDate(date.getUTCDate() - 1);
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.addOneYearMinusDay = addOneYearMinusDay;
77
+ exports.addMonths = addMonths;
72
78
  /**
73
- * The leave year containing `date` for an organization whose year starts on the first of
74
- * `startMonth` (1-12, `organizations.leave_calendar_start_month`). A March start puts
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 leavePeriodFor = (date, startMonth) => {
78
- const parsed = (0, exports.parseIsoDate)(date);
79
- const year = parsed.getUTCMonth() + 1 >= startMonth ? parsed.getUTCFullYear() : parsed.getUTCFullYear() - 1;
80
- const period_start = (0, exports.toIsoDate)(new Date(Date.UTC(year, startMonth - 1, 1)));
81
- return { period_start, period_end: (0, exports.addOneYearMinusDay)(period_start) };
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.leavePeriodFor = leavePeriodFor;
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;
@@ -59,4 +59,5 @@ export declare class UpdateOrganizationDto implements UpdateOrganizationDtoType
59
59
  readonly industry?: string | null;
60
60
  readonly marketing_consent?: boolean;
61
61
  readonly currency?: string;
62
+ readonly leave_calendar_start_month?: number;
62
63
  }
@@ -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 whose leave year starts on the first of `month` (1-12) - the tenants the
101
- * nightly rollover visits on that day. Unscoped like the reconcile: the cron picks its tenants
102
- * from here. `modules` comes along so the caller can skip an organization without HR.
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
- organizationsWithLeaveYearStartingIn(month: number): Promise<{
109
+ organizationsWithModule(moduleKey: string): Promise<{
105
110
  mms_id: string;
106
- modules: string[];
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 whose leave year starts on the first of `month` (1-12) - the tenants the
171
- * nightly rollover visits on that day. Unscoped like the reconcile: the cron picks its tenants
172
- * from here. `modules` comes along so the caller can skip an organization without HR.
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 organizationsWithLeaveYearStartingIn(month) {
188
+ async organizationsWithModule(moduleKey) {
175
189
  const { data, error } = await this.supabase
176
190
  .from('organizations')
177
- .select('mms_id, modules')
178
- .eq('leave_calendar_start_month', month);
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.
@@ -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;
package/dist/users/dto.js CHANGED
@@ -94,6 +94,8 @@ class GetUsersQueryDto {
94
94
  page_size = 10;
95
95
  // Comma-joined booleans on the wire, e.g. "true,false". Absent = all users.
96
96
  active_in;
97
+ // Still v4: a manager is a `users.mms_id`, which is the GoTrue auth uid (see UserIdDto).
98
+ manager_id;
97
99
  }
98
100
  exports.GetUsersQueryDto = GetUsersQueryDto;
99
101
  __decorate([
@@ -126,6 +128,12 @@ __decorate([
126
128
  (0, class_validator_1.IsBoolean)({ each: true, message: 'Active filter values must be booleans' }),
127
129
  __metadata("design:type", Object)
128
130
  ], GetUsersQueryDto.prototype, "active_in", void 0);
131
+ __decorate([
132
+ (0, class_validator_1.IsOptional)(),
133
+ (0, validators_1.IsNullable)(),
134
+ (0, class_validator_1.IsUUID)('4', { message: 'Invalid manager ID format' }),
135
+ __metadata("design:type", Object)
136
+ ], GetUsersQueryDto.prototype, "manager_id", void 0);
129
137
  class UpdateUserDto {
130
138
  name;
131
139
  surname;
@@ -138,6 +146,7 @@ class UpdateUserDto {
138
146
  job_title;
139
147
  manager_id;
140
148
  secondary_manager_id;
149
+ working_days;
141
150
  social_facebook;
142
151
  social_linkedin;
143
152
  social_twitter;
@@ -216,6 +225,17 @@ __decorate([
216
225
  (0, class_validator_1.IsUUID)('4', { message: 'Invalid secondary manager ID format' }),
217
226
  __metadata("design:type", Object)
218
227
  ], UpdateUserDto.prototype, "secondary_manager_id", void 0);
228
+ __decorate([
229
+ (0, class_validator_1.IsOptional)(),
230
+ (0, class_validator_1.IsArray)({ message: 'Working days must be a list of weekdays' }),
231
+ (0, class_validator_1.ArrayMinSize)(1, { message: 'Choose at least one working day' }),
232
+ (0, class_validator_1.ArrayMaxSize)(7, { message: 'A week has seven days' }),
233
+ (0, class_validator_1.ArrayUnique)({ message: 'Each working day may appear once' }),
234
+ (0, class_validator_1.IsInt)({ each: true, message: 'Working days are weekday numbers' }),
235
+ (0, class_validator_1.Min)(1, { each: true, message: 'Working days run from 1 (Monday) to 7 (Sunday)' }),
236
+ (0, class_validator_1.Max)(7, { each: true, message: 'Working days run from 1 (Monday) to 7 (Sunday)' }),
237
+ __metadata("design:type", Array)
238
+ ], UpdateUserDto.prototype, "working_days", void 0);
219
239
  __decorate([
220
240
  (0, class_validator_1.IsOptional)(),
221
241
  (0, validators_1.EmptyToNull)(),
@@ -5,6 +5,13 @@ import type { TypedSupabaseClient } from '../client';
5
5
  import type { GetUsersQueryDto } from './dto';
6
6
  export * from './dto';
7
7
  export * from './paths';
8
+ /** What leave needs to know about someone's employment. Dates are Johannesburg calendar dates. */
9
+ export type UserEmploymentRow = UserIdentity & {
10
+ active: boolean;
11
+ start_date: string;
12
+ end_date: string | null;
13
+ working_days: number[];
14
+ };
8
15
  /** The full users row. The api resolves its manager/created_by/last_updated_by ids into identities. */
9
16
  export type UserRow = Record<string, unknown> & {
10
17
  mms_id: string;
@@ -33,8 +40,11 @@ export declare class UsersResource {
33
40
  * `active` is not consulted: a deactivated report's history stays visible to their manager.
34
41
  */
35
42
  idsManagedBy(managerId: string): Promise<string[]>;
36
- /** The ids of every active member - who the leave rollover grants a new period to. */
37
- activeIds(): Promise<string[]>;
43
+ /**
44
+ * Employment details for leave: start and end as calendar dates in Johannesburg (the columns
45
+ * are timestamps), and the working week. Everyone in the organization, or just `userIds`.
46
+ */
47
+ employment(userIds?: string[]): Promise<UserEmploymentRow[]>;
38
48
  /** Seats in use. Counts rows rather than fetching them. */
39
49
  activeCount(): Promise<number>;
40
50
  isActive(userId: string): Promise<boolean>;
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.UsersResource = void 0;
18
18
  const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
19
+ const working_days_1 = require("../leave/working-days");
19
20
  const audit_1 = require("../audit");
20
21
  const errors_1 = require("../errors");
21
22
  const paths_1 = require("../supporting-files/paths");
@@ -58,6 +59,10 @@ class UsersResource {
58
59
  if (trimmedSearch) {
59
60
  usersQuery = usersQuery.or((0, transforms_1.orIlike)(['name', 'surname', 'email'], trimmedSearch));
60
61
  }
62
+ // Either manager line, as `idsManagedBy` reads it. PostgREST ANDs this with the search.
63
+ if (query.manager_id) {
64
+ usersQuery = usersQuery.or(`manager_id.eq.${query.manager_id},secondary_manager_id.eq.${query.manager_id}`);
65
+ }
61
66
  const { data, error, count } = await usersQuery;
62
67
  if (error)
63
68
  throw (0, errors_1.mapPostgrestError)(error);
@@ -107,16 +112,26 @@ class UsersResource {
107
112
  throw (0, errors_1.mapPostgrestError)(error);
108
113
  return (data ?? []).map((row) => row.mms_id);
109
114
  }
110
- /** The ids of every active member - who the leave rollover grants a new period to. */
111
- async activeIds() {
112
- const { data, error } = await this.supabase
115
+ /**
116
+ * Employment details for leave: start and end as calendar dates in Johannesburg (the columns
117
+ * are timestamps), and the working week. Everyone in the organization, or just `userIds`.
118
+ */
119
+ async employment(userIds) {
120
+ let employmentQuery = this.supabase
113
121
  .from('users')
114
- .select('mms_id')
115
- .eq('organization_id', this.auth.organization_id)
116
- .eq('active', true);
122
+ .select('mms_id, name, surname, email, profile_image, active, start_date, end_date, working_days')
123
+ .eq('organization_id', this.auth.organization_id);
124
+ if (userIds) {
125
+ employmentQuery = employmentQuery.in('mms_id', userIds);
126
+ }
127
+ const { data, error } = await employmentQuery;
117
128
  if (error)
118
129
  throw (0, errors_1.mapPostgrestError)(error);
119
- return (data ?? []).map((row) => row.mms_id);
130
+ return (data ?? []).map((row) => ({
131
+ ...row,
132
+ start_date: (0, working_days_1.todayInJohannesburg)(new Date(row.start_date)),
133
+ end_date: row.end_date ? (0, working_days_1.todayInJohannesburg)(new Date(row.end_date)) : null,
134
+ }));
120
135
  }
121
136
  /** Seats in use. Counts rows rather than fetching them. */
122
137
  async activeCount() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@managemint-solutions/sdk",
3
- "version": "0.45.0",
3
+ "version": "0.49.0",
4
4
  "description": "Typed Supabase data-access SDK for ManageMint Solutions",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Scott Bebington <scottbebington@gmail.com>",
@@ -51,7 +51,7 @@
51
51
  "testEnvironment": "node"
52
52
  },
53
53
  "dependencies": {
54
- "@managemint-solutions/entities": "^1.28.0"
54
+ "@managemint-solutions/entities": "^1.32.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@nestjs/common": "^11.0.0",