@managemint-solutions/sdk 0.39.0 → 0.40.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.
Files changed (38) hide show
  1. package/README.md +75 -3
  2. package/dist/client.d.ts +3 -0
  3. package/dist/client.js +5 -0
  4. package/dist/index.d.ts +4 -1
  5. package/dist/index.js +14 -1
  6. package/dist/leave/dto.d.ts +4 -0
  7. package/dist/leave/dto.js +24 -0
  8. package/dist/leave/leave-entitlements.d.ts +10 -3
  9. package/dist/leave/leave-entitlements.js +26 -6
  10. package/dist/leave/leave-requests.d.ts +2 -3
  11. package/dist/leave/leave-requests.js +2 -1
  12. package/dist/leave/leave-types.d.ts +2 -3
  13. package/dist/leave/leave-types.js +3 -0
  14. package/dist/leave/working-days.d.ts +4 -0
  15. package/dist/leave/working-days.js +13 -5
  16. package/dist/permissions/dto.d.ts +4 -0
  17. package/dist/permissions/dto.js +20 -0
  18. package/dist/service-client/index.d.ts +13 -0
  19. package/dist/service-client/index.js +22 -0
  20. package/dist/timesheets/dto.d.ts +42 -0
  21. package/dist/timesheets/dto.js +211 -0
  22. package/dist/timesheets/index.d.ts +4 -0
  23. package/dist/timesheets/index.js +27 -0
  24. package/dist/timesheets/timesheet-entries.d.ts +54 -0
  25. package/dist/timesheets/timesheet-entries.js +141 -0
  26. package/dist/timesheets/timesheets.d.ts +67 -0
  27. package/dist/timesheets/timesheets.js +201 -0
  28. package/dist/timesheets/weeks.d.ts +7 -0
  29. package/dist/timesheets/weeks.js +26 -0
  30. package/dist/transforms.d.ts +13 -0
  31. package/dist/transforms.js +24 -1
  32. package/dist/users/dto.d.ts +1 -0
  33. package/dist/users/dto.js +7 -0
  34. package/dist/users/index.d.ts +8 -1
  35. package/dist/users/index.js +17 -2
  36. package/dist/validators.d.ts +10 -0
  37. package/dist/validators.js +21 -1
  38. package/package.json +2 -2
package/README.md CHANGED
@@ -275,8 +275,11 @@ const deleted = await users.remove(userId); // hard
275
275
  ```
276
276
 
277
277
  Cross-tenant lookups - the organization behind a Paystack code, a ledger row by id, every
278
- organization with a Paystack customer - hang off `service` itself, because a webhook or cron does
279
- not know its tenant until it looks one up.
278
+ organization with a Paystack customer, every organization whose leave year starts in a given month
279
+ (`organizationsWithLeaveYearStartingIn(month)` → `{ mms_id, modules }[]`)
280
+ - hang off `service` itself, because a webhook or cron does not know its tenant until it looks one
281
+ up. The scope also carries `leaveTypes`, `leaveEntitlements` and `leaveRequests` for the leave-year
282
+ rollover, whose writes are attributed to the system.
280
283
 
281
284
  `update` / `transition` / `remove` pre-read the flat row (by `mms_id`, and by `organization_id`
282
285
  unless `scopeByOrganization: false` - the `organizations` table is its own tenant), so a missing
@@ -424,7 +427,7 @@ const today = todayInJohannesburg(); // "
424
427
 
425
428
  ```ts
426
429
  const types = await supabase.leaveTypes.list({ active: true });
427
- const annual = await supabase.leaveTypes.create({ name: 'Annual', description: null, default_days: 15, is_paid: true, requires_approval: true });
430
+ const annual = await supabase.leaveTypes.create({ name: 'Annual', description: null, default_days: 15, is_paid: true, requires_approval: true, requires_document: false });
428
431
  await supabase.leaveTypes.remove(annual.mms_id); // hard delete; 409 once a request or entitlement references it
429
432
 
430
433
  const taken = await supabase.leaveEntitlements.overlaps(userId, annual.mms_id, '2026-01-01', '2026-12-31'); // an entitlement of that type already covers part of the period
@@ -440,6 +443,27 @@ const annualOnly = await supabase.leaveRequests.list({ ...query, leave_type_id:
440
443
  const reports = await supabase.leaveRequests.list(query, { userIds: [me, ...(await supabase.users.idsManagedBy(me))] });
441
444
  ```
442
445
 
446
+ A leave type with `requires_document` set is one the api will not approve until a supporting file
447
+ is attached to the request; the `leave_type` embed on requests and entitlements carries the flag so
448
+ a caller need not fetch the type. The reporting line is two deep on the user: `manager_id` and
449
+ `secondary_manager_id` (`UpdateUserDto`, both v4 user ids, nullable), and
450
+ `users.idsManagedBy(managerId)` returns everyone naming that person as either - one level down,
451
+ `active` ignored.
452
+
453
+ An entitlement carries `max_carry_over_days` (HR-set, default 0): on the leave-year boundary the
454
+ api's nightly rollover creates next period's entitlement for each person and type as
455
+ `default_days + min(unused, cap)`. What it runs on:
456
+
457
+ ```ts
458
+ import { leavePeriodFor, shiftDays } from '@managemint-solutions/sdk';
459
+
460
+ const scope = service.forOrganization(organizationId); // system actor: created_by stays null
461
+ const ended = leavePeriodFor(shiftDays(today, -1), startMonth); // the period that closed yesterday
462
+ const rows = await scope.leaveEntitlements.endingOn(ended.period_end); // every entitlement ending that day, balances included, paged
463
+ const active = new Set(await scope.users.activeIds()); // who still gets a new period
464
+ await scope.leaveEntitlements.create({ ...next, user_id, leave_type_id, days_entitled, max_carry_over_days, notes });
465
+ ```
466
+
443
467
  `review`, `cancel` and `update` are compare-and-set writes (`updateWhere`): they land only while
444
468
  the request is still in the status they expect and resolve to `null` otherwise, so two reviewers
445
469
  cannot both decide the same request. The `leave_requests` table also carries an exclusion
@@ -448,6 +472,54 @@ constraint so two overlapping live requests of one user cannot both be inserted
448
472
  Notes, additional details and supporting files may attach to leave as well: their `entity`
449
473
  accepts `leave_requests` and `leave_entitlements` alongside the CRM entity types.
450
474
 
475
+ ## Timesheets
476
+
477
+ The second slice of the HR module: `timesheet_entries` (what an employee logs - a date, a start and
478
+ end `HH:MM`, optionally a client / project / task, a description) grouped into `timesheets`, one
479
+ row per person and Monday-to-Sunday week, which the employee submits and a manager approves or
480
+ rejects with a note. Both are org-scoped audited resources on the caller's client. A week's
481
+ `total_minutes` / `entry_count` are counted on read from its entries, never stored;
482
+ `duration_minutes` on an entry is generated by Postgres. Times are read back trimmed to `HH:MM`
483
+ (the column is `time`), so a row can be sent straight back in; `24:00` is a valid end for a shift
484
+ finishing at midnight, and an entry never crosses midnight - split it.
485
+
486
+ The week arithmetic is plain exported functions, shared with the api:
487
+
488
+ ```ts
489
+ import { minutesBetween, weekStartOf } from '@managemint-solutions/sdk';
490
+
491
+ weekStartOf('2026-09-27'); // '2026-09-21' - a Sunday belongs to the week before it
492
+ minutesBetween('22:00', '24:00'); // 120; zero or negative when the end is not after the start
493
+ ```
494
+
495
+ ```ts
496
+ const weekStart = weekStartOf(body.date);
497
+ const week =
498
+ (await supabase.timesheets.findFor(me, weekStart)) ?? // { mms_id, status, week_start } or null
499
+ (await supabase.timesheets.create({ user_id: me, week_start: weekStart })); // opened as a draft
500
+
501
+ const clash = await supabase.timesheetEntries.overlaps(me, body.date, body.start_time, body.end_time); // same day, sharing a minute
502
+ const entry = await supabase.timesheetEntries.create({ timesheet_id: week.mms_id, user_id: me, ...body });
503
+ await supabase.timesheetEntries.update(entry.mms_id, { timesheet_id: week.mms_id, ...body });
504
+ await supabase.timesheetEntries.remove(entry.mms_id);
505
+
506
+ const mine = await supabase.timesheets.list({ ...query, user_id: me }); // one list; user_id / status / week_start narrow it
507
+ const reports = await supabase.timesheets.list(query, { userIds: [me, ...(await supabase.users.idsManagedBy(me))] });
508
+ const detail = await supabase.timesheets.getById(week.mms_id); // summary + entries
509
+ const submitted = await supabase.timesheets.submit(week.mms_id); // null = no longer draft or rejected
510
+ const reviewed = await supabase.timesheets.review(week.mms_id, { status: TimesheetReviewDecision.APPROVED, reviewed_by: managerId, review_note: null }); // null = no longer submitted
511
+
512
+ // The report: the api aggregates flat rows, keeping only the entries of approved weeks.
513
+ const approved = await supabase.timesheets.approvedIds(userIds, date_from, date_to);
514
+ const rows = (await supabase.timesheetEntries.forReport(userIds, date_from, date_to)).filter((row) => approved.has(row.timesheet_id));
515
+ ```
516
+
517
+ `submit` and `review` are compare-and-set writes (`updateWhere`): `submit` lands only on a draft or
518
+ rejected week (and clears the previous decision), `review` only on a submitted one, and each
519
+ resolves to `null` otherwise. The `timesheet_entries` table also carries an exclusion constraint so
520
+ two overlapping entries of one user cannot both be inserted in a race. `forReport`, `approvedIds`
521
+ and the per-page totals walk their tables in pages of 1000, PostgREST's row cap.
522
+
451
523
  ## Public holidays
452
524
 
453
525
  The public-holiday calendar, `public_holidays`, on the caller's client as `supabase.publicHolidays`.
package/dist/client.d.ts CHANGED
@@ -16,6 +16,7 @@ import { PublicHolidaysResource } from './public-holidays';
16
16
  import { StatusesResource } from './statuses';
17
17
  import { SupportingFilesResource } from './supporting-files';
18
18
  import { TasksResource } from './tasks';
19
+ import { TimesheetEntriesResource, TimesheetsResource } from './timesheets';
19
20
  import { UsersResource } from './users';
20
21
  export type TypedSupabaseClient = SupabaseJsClient;
21
22
  export type SupabaseClientConfig = {
@@ -62,6 +63,8 @@ export declare class SupabaseClient {
62
63
  readonly publicHolidays: PublicHolidaysResource;
63
64
  readonly leaveEntitlements: LeaveEntitlementsResource;
64
65
  readonly leaveRequests: LeaveRequestsResource;
66
+ readonly timesheets: TimesheetsResource;
67
+ readonly timesheetEntries: TimesheetEntriesResource;
65
68
  constructor(config: SupabaseClientConfig);
66
69
  }
67
70
  export declare const createSupabaseClient: (config: SupabaseClientConfig) => SupabaseClient;
package/dist/client.js CHANGED
@@ -18,6 +18,7 @@ const public_holidays_1 = require("./public-holidays");
18
18
  const statuses_1 = require("./statuses");
19
19
  const supporting_files_1 = require("./supporting-files");
20
20
  const tasks_1 = require("./tasks");
21
+ const timesheets_1 = require("./timesheets");
21
22
  const users_1 = require("./users");
22
23
  /**
23
24
  * The caller's data client: every query carries their bearer token, so RLS decides what they
@@ -47,6 +48,8 @@ class SupabaseClient {
47
48
  publicHolidays;
48
49
  leaveEntitlements;
49
50
  leaveRequests;
51
+ timesheets;
52
+ timesheetEntries;
50
53
  constructor(config) {
51
54
  this.auth = config.auth;
52
55
  this.supabase =
@@ -72,6 +75,8 @@ class SupabaseClient {
72
75
  this.publicHolidays = new public_holidays_1.PublicHolidaysResource(this.supabase);
73
76
  this.leaveEntitlements = new leave_1.LeaveEntitlementsResource(this.supabase, this.auth, this.audit);
74
77
  this.leaveRequests = new leave_1.LeaveRequestsResource(this.supabase, this.auth, this.audit);
78
+ this.timesheets = new timesheets_1.TimesheetsResource(this.supabase, this.auth, this.audit);
79
+ this.timesheetEntries = new timesheets_1.TimesheetEntriesResource(this.supabase, this.auth, this.audit);
75
80
  }
76
81
  }
77
82
  exports.SupabaseClient = SupabaseClient;
package/dist/index.d.ts CHANGED
@@ -39,5 +39,8 @@ export { CreateTaskDto, GetTasksDto, TaskIdDto, UpdateTaskDto } from './tasks';
39
39
  export type { CreateLeaveEntitlementInput, CreateLeaveRequestRow, LeaveEntitlementCover, ReviewLeaveRequestRow, UpdateLeaveRequestRow, } from './leave';
40
40
  export { CreateLeaveEntitlementDto, CreateLeaveRequestDto, CreateLeaveTypeDto, GetLeaveEntitlementsDto, GetLeaveRequestsDto, GetLeaveTypesDto, LeaveEntitlementIdDto, LeaveRequestIdDto, LeaveTypeIdDto, ReviewLeaveRequestDto, UpdateLeaveEntitlementDto, UpdateLeaveRequestDto, UpdateLeaveTypeDto, } from './leave';
41
41
  export { GetPublicHolidaysDto } from './public-holidays';
42
- export { addOneYearMinusDay, countWorkingDays, isWithinPeriod, leavePeriodFor, todayInJohannesburg, } from './leave';
42
+ export { addOneYearMinusDay, countWorkingDays, isWithinPeriod, leavePeriodFor, shiftDays, todayInJohannesburg, } from './leave';
43
+ export type { CreateTimesheetEntryRow, CreateTimesheetRow, ReviewTimesheetRow, TimesheetCover, TimesheetReportRow, UpdateTimesheetEntryRow, } from './timesheets';
44
+ export { CreateTimesheetEntryDto, GetTimesheetReportDto, GetTimesheetsDto, ReviewTimesheetDto, TimesheetEntryIdDto, TimesheetIdDto, UpdateTimesheetEntryDto, } from './timesheets';
45
+ export { minutesBetween, weekStartOf } from './timesheets';
43
46
  export { monthlyTotalMinor } from './pricebooks';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // to keep. The row and input types they produce and accept are exported for annotations.
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.UserIdDto = exports.UpdateUserDto = exports.UpdateMyProfileDto = exports.GetUsersQueryDto = exports.AddUserDto = exports.UpdatePermissionsDto = exports.PermissionsResource = exports.UpdateOrganizationDto = exports.CreateOrganizationDto = exports.NotificationIdDto = exports.GetNotificationsDto = exports.NoteIdDto = exports.GetNotesDto = exports.AddNoteDto = exports.UpdateAdditionalDetailsDto = exports.IsAdditionalDetailsValueByType = exports.GetAdditionalDetailsDto = exports.AdditionalDetailsIdDto = exports.AddAdditionalDetailsDto = exports.UpdateStatusDto = exports.StatusIdDto = exports.GetStatusesDto = exports.AddStatusDto = exports.CreateSupportRequestDto = exports.UpdateConfigDto = exports.CreateConfigDto = exports.CreateFeatureRequestDto = exports.InvoiceIdDto = exports.GetInvoicesQueryDto = exports.ReinstateSubscriptionDto = exports.PurchaseModuleDto = exports.PaymentIdDto = exports.InitialPaymentDto = exports.GetPaymentsQueryDto = exports.CancelSubscriptionDto = exports.GetAuditTrailDto = exports.IsNullable = exports.EmptyToNull = exports.SupabaseClientError = exports.createSupabaseServiceClient = exports.SupabaseServiceClient = exports.SignInDto = exports.SendPasswordResetEmailDto = exports.ResetPasswordDto = exports.ExchangeTokenHashDto = exports.ExchangeRefreshTokenDto = exports.createSupabaseAuthClient = exports.SupabaseAuthClient = exports.createSupabaseClient = exports.SupabaseClient = void 0;
11
- exports.monthlyTotalMinor = exports.todayInJohannesburg = exports.leavePeriodFor = exports.isWithinPeriod = exports.countWorkingDays = exports.addOneYearMinusDay = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.UpdateTaskDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = void 0;
11
+ exports.monthlyTotalMinor = exports.weekStartOf = exports.minutesBetween = exports.UpdateTimesheetEntryDto = exports.TimesheetIdDto = exports.TimesheetEntryIdDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.GetTimesheetReportDto = exports.CreateTimesheetEntryDto = exports.todayInJohannesburg = exports.shiftDays = exports.leavePeriodFor = exports.isWithinPeriod = exports.countWorkingDays = exports.addOneYearMinusDay = exports.GetPublicHolidaysDto = exports.UpdateLeaveTypeDto = exports.UpdateLeaveRequestDto = exports.UpdateLeaveEntitlementDto = exports.ReviewLeaveRequestDto = exports.LeaveTypeIdDto = exports.LeaveRequestIdDto = exports.LeaveEntitlementIdDto = exports.GetLeaveTypesDto = exports.GetLeaveRequestsDto = exports.GetLeaveEntitlementsDto = exports.CreateLeaveTypeDto = exports.CreateLeaveRequestDto = exports.CreateLeaveEntitlementDto = exports.UpdateTaskDto = exports.TaskIdDto = exports.GetTasksDto = exports.CreateTaskDto = exports.UpdateProjectDto = exports.ProjectIdDto = exports.GetProjectsDto = exports.CreateProjectDto = exports.UpdateClientDto = exports.GetClientsDto = exports.CreateClientDto = exports.ClientIdDto = exports.SupportingFileIdDto = exports.GetSupportingFilesDto = void 0;
12
12
  var client_1 = require("./client");
13
13
  Object.defineProperty(exports, "SupabaseClient", { enumerable: true, get: function () { return client_1.SupabaseClient; } });
14
14
  Object.defineProperty(exports, "createSupabaseClient", { enumerable: true, get: function () { return client_1.createSupabaseClient; } });
@@ -122,6 +122,19 @@ Object.defineProperty(exports, "addOneYearMinusDay", { enumerable: true, get: fu
122
122
  Object.defineProperty(exports, "countWorkingDays", { enumerable: true, get: function () { return leave_2.countWorkingDays; } });
123
123
  Object.defineProperty(exports, "isWithinPeriod", { enumerable: true, get: function () { return leave_2.isWithinPeriod; } });
124
124
  Object.defineProperty(exports, "leavePeriodFor", { enumerable: true, get: function () { return leave_2.leavePeriodFor; } });
125
+ Object.defineProperty(exports, "shiftDays", { enumerable: true, get: function () { return leave_2.shiftDays; } });
125
126
  Object.defineProperty(exports, "todayInJohannesburg", { enumerable: true, get: function () { return leave_2.todayInJohannesburg; } });
127
+ var timesheets_1 = require("./timesheets");
128
+ Object.defineProperty(exports, "CreateTimesheetEntryDto", { enumerable: true, get: function () { return timesheets_1.CreateTimesheetEntryDto; } });
129
+ Object.defineProperty(exports, "GetTimesheetReportDto", { enumerable: true, get: function () { return timesheets_1.GetTimesheetReportDto; } });
130
+ Object.defineProperty(exports, "GetTimesheetsDto", { enumerable: true, get: function () { return timesheets_1.GetTimesheetsDto; } });
131
+ Object.defineProperty(exports, "ReviewTimesheetDto", { enumerable: true, get: function () { return timesheets_1.ReviewTimesheetDto; } });
132
+ Object.defineProperty(exports, "TimesheetEntryIdDto", { enumerable: true, get: function () { return timesheets_1.TimesheetEntryIdDto; } });
133
+ Object.defineProperty(exports, "TimesheetIdDto", { enumerable: true, get: function () { return timesheets_1.TimesheetIdDto; } });
134
+ Object.defineProperty(exports, "UpdateTimesheetEntryDto", { enumerable: true, get: function () { return timesheets_1.UpdateTimesheetEntryDto; } });
135
+ // The week arithmetic, like the leave calendar: plain functions the api resolves a week with.
136
+ var timesheets_2 = require("./timesheets");
137
+ Object.defineProperty(exports, "minutesBetween", { enumerable: true, get: function () { return timesheets_2.minutesBetween; } });
138
+ Object.defineProperty(exports, "weekStartOf", { enumerable: true, get: function () { return timesheets_2.weekStartOf; } });
126
139
  var pricebooks_1 = require("./pricebooks");
127
140
  Object.defineProperty(exports, "monthlyTotalMinor", { enumerable: true, get: function () { return pricebooks_1.monthlyTotalMinor; } });
@@ -12,6 +12,7 @@ export declare class CreateLeaveTypeDto implements CreateLeaveTypeDtoType {
12
12
  readonly default_days: number;
13
13
  readonly is_paid: boolean;
14
14
  readonly requires_approval: boolean;
15
+ readonly requires_document: boolean;
15
16
  }
16
17
  export declare class UpdateLeaveTypeDto implements UpdateLeaveTypeDtoType {
17
18
  readonly name: string;
@@ -19,6 +20,7 @@ export declare class UpdateLeaveTypeDto implements UpdateLeaveTypeDtoType {
19
20
  readonly default_days: number;
20
21
  readonly is_paid: boolean;
21
22
  readonly requires_approval: boolean;
23
+ readonly requires_document: boolean;
22
24
  readonly active: boolean;
23
25
  }
24
26
  export declare class LeaveEntitlementIdDto implements LeaveEntitlementIdDtoType {
@@ -35,12 +37,14 @@ export declare class CreateLeaveEntitlementDto implements CreateLeaveEntitlement
35
37
  readonly user_id: string;
36
38
  readonly leave_type_id: string;
37
39
  readonly days_entitled: number;
40
+ readonly max_carry_over_days: number;
38
41
  readonly period_start: string | null;
39
42
  readonly period_end: string | null;
40
43
  readonly notes: string | null;
41
44
  }
42
45
  export declare class UpdateLeaveEntitlementDto implements UpdateLeaveEntitlementDtoType {
43
46
  readonly days_entitled: number;
47
+ readonly max_carry_over_days: number;
44
48
  readonly period_end: string;
45
49
  readonly notes: string | null;
46
50
  }
package/dist/leave/dto.js CHANGED
@@ -52,6 +52,7 @@ class CreateLeaveTypeDto {
52
52
  default_days;
53
53
  is_paid;
54
54
  requires_approval;
55
+ requires_document;
55
56
  }
56
57
  exports.CreateLeaveTypeDto = CreateLeaveTypeDto;
57
58
  __decorate([
@@ -82,12 +83,17 @@ __decorate([
82
83
  (0, class_validator_1.IsBoolean)({ message: 'Requires approval must be true or false' }),
83
84
  __metadata("design:type", Boolean)
84
85
  ], CreateLeaveTypeDto.prototype, "requires_approval", void 0);
86
+ __decorate([
87
+ (0, class_validator_1.IsBoolean)({ message: 'Requires document must be true or false' }),
88
+ __metadata("design:type", Boolean)
89
+ ], CreateLeaveTypeDto.prototype, "requires_document", void 0);
85
90
  class UpdateLeaveTypeDto {
86
91
  name;
87
92
  description;
88
93
  default_days;
89
94
  is_paid;
90
95
  requires_approval;
96
+ requires_document;
91
97
  active;
92
98
  }
93
99
  exports.UpdateLeaveTypeDto = UpdateLeaveTypeDto;
@@ -119,6 +125,10 @@ __decorate([
119
125
  (0, class_validator_1.IsBoolean)({ message: 'Requires approval must be true or false' }),
120
126
  __metadata("design:type", Boolean)
121
127
  ], UpdateLeaveTypeDto.prototype, "requires_approval", void 0);
128
+ __decorate([
129
+ (0, class_validator_1.IsBoolean)({ message: 'Requires document must be true or false' }),
130
+ __metadata("design:type", Boolean)
131
+ ], UpdateLeaveTypeDto.prototype, "requires_document", void 0);
122
132
  __decorate([
123
133
  (0, class_validator_1.IsBoolean)({ message: 'Active must be true or false' }),
124
134
  __metadata("design:type", Boolean)
@@ -177,6 +187,7 @@ class CreateLeaveEntitlementDto {
177
187
  user_id;
178
188
  leave_type_id;
179
189
  days_entitled;
190
+ max_carry_over_days;
180
191
  period_start;
181
192
  period_end;
182
193
  notes;
@@ -196,6 +207,12 @@ __decorate([
196
207
  (0, class_validator_1.Max)(999.9, { message: 'Days entitled cannot exceed 999.9' }),
197
208
  __metadata("design:type", Number)
198
209
  ], CreateLeaveEntitlementDto.prototype, "days_entitled", void 0);
210
+ __decorate([
211
+ (0, class_validator_1.IsNumber)({ maxDecimalPlaces: 1 }, { message: 'Max carry over days must be a number with at most one decimal' }),
212
+ (0, class_validator_1.Min)(0, { message: 'Max carry over days cannot be negative' }),
213
+ (0, class_validator_1.Max)(999.9, { message: 'Max carry over days cannot exceed 999.9' }),
214
+ __metadata("design:type", Number)
215
+ ], CreateLeaveEntitlementDto.prototype, "max_carry_over_days", void 0);
199
216
  __decorate([
200
217
  (0, validators_1.IsNullable)(),
201
218
  (0, validators_1.IsCalendarDate)({ message: 'Period start must be a calendar date (YYYY-MM-DD)' }),
@@ -215,6 +232,7 @@ __decorate([
215
232
  ], CreateLeaveEntitlementDto.prototype, "notes", void 0);
216
233
  class UpdateLeaveEntitlementDto {
217
234
  days_entitled;
235
+ max_carry_over_days;
218
236
  period_end;
219
237
  notes;
220
238
  }
@@ -225,6 +243,12 @@ __decorate([
225
243
  (0, class_validator_1.Max)(999.9, { message: 'Days entitled cannot exceed 999.9' }),
226
244
  __metadata("design:type", Number)
227
245
  ], UpdateLeaveEntitlementDto.prototype, "days_entitled", void 0);
246
+ __decorate([
247
+ (0, class_validator_1.IsNumber)({ maxDecimalPlaces: 1 }, { message: 'Max carry over days must be a number with at most one decimal' }),
248
+ (0, class_validator_1.Min)(0, { message: 'Max carry over days cannot be negative' }),
249
+ (0, class_validator_1.Max)(999.9, { message: 'Max carry over days cannot exceed 999.9' }),
250
+ __metadata("design:type", Number)
251
+ ], UpdateLeaveEntitlementDto.prototype, "max_carry_over_days", void 0);
228
252
  __decorate([
229
253
  (0, validators_1.IsCalendarDate)({ message: 'Period end must be a calendar date (YYYY-MM-DD)' }),
230
254
  __metadata("design:type", String)
@@ -1,7 +1,6 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { LeaveEntitlementEntity, LeavePeriod } from '@managemint-solutions/entities/leave';
3
- import type { AuthContext } from '../auth';
4
- import { type AuditResource } from '../audit';
3
+ import { type AuditActor, type AuditResource } from '../audit';
5
4
  import type { TypedSupabaseClient } from '../client';
6
5
  import type { GetLeaveEntitlementsDto, UpdateLeaveEntitlementDto } from './dto';
7
6
  /** What the request path needs of the entitlement covering a date: the period and the cap. */
@@ -14,13 +13,14 @@ export type CreateLeaveEntitlementInput = LeavePeriod & {
14
13
  user_id: string;
15
14
  leave_type_id: string;
16
15
  days_entitled: number;
16
+ max_carry_over_days: number;
17
17
  notes?: string | null;
18
18
  };
19
19
  export declare class LeaveEntitlementsResource {
20
20
  private readonly supabase;
21
21
  private readonly auth;
22
22
  private readonly table;
23
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
23
+ constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
24
24
  list(query: GetLeaveEntitlementsDto): Promise<MmsList<LeaveEntitlementEntity>>;
25
25
  getById(leaveEntitlementId: string): Promise<LeaveEntitlementEntity>;
26
26
  /**
@@ -35,6 +35,12 @@ export declare class LeaveEntitlementsResource {
35
35
  * that survives a race. `excludeId` leaves out the entitlement being edited.
36
36
  */
37
37
  overlaps(userId: string, leaveTypeId: string, start: string, end: string, excludeId?: string): Promise<boolean>;
38
+ /**
39
+ * Every entitlement of the organization whose period ends on `periodEnd`, balances included -
40
+ * what the leave-year rollover carries forward. Walks the table in pages so a large tenant is
41
+ * not cut short by the row cap.
42
+ */
43
+ endingOn(periodEnd: string): Promise<LeaveEntitlementEntity[]>;
38
44
  create(input: CreateLeaveEntitlementInput): Promise<LeaveEntitlementEntity>;
39
45
  update(leaveEntitlementId: string, input: UpdateLeaveEntitlementDto): Promise<LeaveEntitlementEntity>;
40
46
  /** Hard delete: the type simply becomes untracked for that user and period. */
@@ -43,6 +49,7 @@ export declare class LeaveEntitlementsResource {
43
49
  * Adds the computed balance to each row from one query over the requests that could count
44
50
  * against any of them - the approved and pending ones of these users and types, starting in
45
51
  * the span the periods cover. Nothing is stored: a balance is always what the requests say.
52
+ * Paged, since `endingOn` hands it a whole tenant at once.
46
53
  */
47
54
  private withBalances;
48
55
  }
@@ -15,11 +15,13 @@ const LEAVE_ENTITLEMENT_SELECT = `
15
15
  user:user_profiles!user_id (*),
16
16
  leave_type:leave_types!leave_type_id (
17
17
  mms_id,
18
- name
18
+ name,
19
+ requires_document
19
20
  ),
20
21
  period_start,
21
22
  period_end,
22
23
  days_entitled,
24
+ max_carry_over_days,
23
25
  notes
24
26
  `;
25
27
  class LeaveEntitlementsResource {
@@ -116,6 +118,21 @@ class LeaveEntitlementsResource {
116
118
  throw (0, errors_1.mapPostgrestError)(error);
117
119
  return (count ?? 0) > 0;
118
120
  }
121
+ /**
122
+ * Every entitlement of the organization whose period ends on `periodEnd`, balances included -
123
+ * what the leave-year rollover carries forward. Walks the table in pages so a large tenant is
124
+ * not cut short by the row cap.
125
+ */
126
+ async endingOn(periodEnd) {
127
+ const rows = await (0, transforms_1.allRows)((from, to) => this.supabase
128
+ .from('leave_entitlements')
129
+ .select(LEAVE_ENTITLEMENT_SELECT)
130
+ .eq('organization_id', this.auth.organization_id)
131
+ .eq('period_end', periodEnd)
132
+ .order('mms_id')
133
+ .range(from, to));
134
+ return this.withBalances(rows);
135
+ }
119
136
  async create(input) {
120
137
  const row = await this.table.insert({
121
138
  user_id: input.user_id,
@@ -123,6 +140,7 @@ class LeaveEntitlementsResource {
123
140
  period_start: input.period_start,
124
141
  period_end: input.period_end,
125
142
  days_entitled: input.days_entitled,
143
+ max_carry_over_days: input.max_carry_over_days,
126
144
  notes: input.notes ?? null,
127
145
  organization_id: this.auth.organization_id,
128
146
  created_by: this.auth.mms_id,
@@ -133,6 +151,7 @@ class LeaveEntitlementsResource {
133
151
  async update(leaveEntitlementId, input) {
134
152
  await this.table.update(leaveEntitlementId, {
135
153
  days_entitled: input.days_entitled,
154
+ max_carry_over_days: input.max_carry_over_days,
136
155
  period_end: input.period_end,
137
156
  notes: input.notes,
138
157
  last_updated_by: this.auth.mms_id,
@@ -148,6 +167,7 @@ class LeaveEntitlementsResource {
148
167
  * Adds the computed balance to each row from one query over the requests that could count
149
168
  * against any of them - the approved and pending ones of these users and types, starting in
150
169
  * the span the periods cover. Nothing is stored: a balance is always what the requests say.
170
+ * Paged, since `endingOn` hands it a whole tenant at once.
151
171
  */
152
172
  async withBalances(rows) {
153
173
  if (rows.length === 0)
@@ -156,7 +176,7 @@ class LeaveEntitlementsResource {
156
176
  const typeIds = [...new Set(rows.map((row) => row.leave_type.mms_id))];
157
177
  const earliest = rows.map((row) => row.period_start).sort()[0];
158
178
  const latest = rows.map((row) => row.period_end).sort().reverse()[0];
159
- const { data, error } = await this.supabase
179
+ const requests = await (0, transforms_1.allRows)((from, to) => this.supabase
160
180
  .from('leave_requests')
161
181
  .select('user_id, leave_type_id, start_date, days, status')
162
182
  .eq('organization_id', this.auth.organization_id)
@@ -164,10 +184,9 @@ class LeaveEntitlementsResource {
164
184
  .in('leave_type_id', typeIds)
165
185
  .in('status', [enum_2.LeaveRequestStatus.APPROVED, enum_2.LeaveRequestStatus.PENDING])
166
186
  .gte('start_date', earliest)
167
- .lte('start_date', latest);
168
- if (error)
169
- throw (0, errors_1.mapPostgrestError)(error);
170
- const requests = (data ?? []);
187
+ .lte('start_date', latest)
188
+ .order('mms_id')
189
+ .range(from, to));
171
190
  return rows.map((row) => {
172
191
  const mine = requests.filter((request) => request.user_id === row.user.mms_id &&
173
192
  request.leave_type_id === row.leave_type.mms_id &&
@@ -182,6 +201,7 @@ class LeaveEntitlementsResource {
182
201
  return {
183
202
  ...row,
184
203
  days_entitled: daysEntitled,
204
+ max_carry_over_days: Number(row.max_carry_over_days),
185
205
  days_used: daysUsed,
186
206
  days_pending: daysPending,
187
207
  days_remaining: daysEntitled - daysUsed - daysPending,
@@ -1,8 +1,7 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { LeavePeriod, LeaveRequestEntity } from '@managemint-solutions/entities/leave';
3
3
  import { LeaveRequestStatus, LeaveReviewDecision } from '@managemint-solutions/entities/leave/enum';
4
- import type { AuthContext } from '../auth';
5
- import { type AuditResource } from '../audit';
4
+ import { type AuditActor, type AuditResource } from '../audit';
6
5
  import type { TypedSupabaseClient } from '../client';
7
6
  import type { GetLeaveRequestsDto } from './dto';
8
7
  /** A request as the api inserts it: the caller's own, with the days already counted. */
@@ -40,7 +39,7 @@ export declare class LeaveRequestsResource {
40
39
  private readonly supabase;
41
40
  private readonly auth;
42
41
  private readonly table;
43
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
42
+ constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
44
43
  list(query: GetLeaveRequestsDto, options?: ListOptions): Promise<MmsList<LeaveRequestEntity>>;
45
44
  getById(leaveRequestId: string): Promise<LeaveRequestEntity>;
46
45
  /**
@@ -15,7 +15,8 @@ const LEAVE_REQUEST_SELECT = `
15
15
  user:user_profiles!user_id (*),
16
16
  leave_type:leave_types!leave_type_id (
17
17
  mms_id,
18
- name
18
+ name,
19
+ requires_document
19
20
  ),
20
21
  start_date,
21
22
  end_date,
@@ -1,13 +1,12 @@
1
1
  import type { LeaveTypeEntity } from '@managemint-solutions/entities/leave';
2
- import type { AuthContext } from '../auth';
3
- import { type AuditResource } from '../audit';
2
+ import { type AuditActor, type AuditResource } from '../audit';
4
3
  import type { TypedSupabaseClient } from '../client';
5
4
  import type { CreateLeaveTypeDto, GetLeaveTypesDto, UpdateLeaveTypeDto } from './dto';
6
5
  export declare class LeaveTypesResource {
7
6
  private readonly supabase;
8
7
  private readonly auth;
9
8
  private readonly table;
10
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
9
+ constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
11
10
  /** Every type by name; `active` narrows to one side of the flag. Small enough not to page. */
12
11
  list(query: GetLeaveTypesDto): Promise<LeaveTypeEntity[]>;
13
12
  getById(leaveTypeId: string): Promise<LeaveTypeEntity>;
@@ -18,6 +18,7 @@ const LEAVE_TYPE_SELECT = `
18
18
  default_days,
19
19
  is_paid,
20
20
  requires_approval,
21
+ requires_document,
21
22
  active
22
23
  `;
23
24
  class LeaveTypesResource {
@@ -65,6 +66,7 @@ class LeaveTypesResource {
65
66
  default_days: input.default_days,
66
67
  is_paid: input.is_paid,
67
68
  requires_approval: input.requires_approval,
69
+ requires_document: input.requires_document,
68
70
  organization_id: this.auth.organization_id,
69
71
  created_by: this.auth.mms_id,
70
72
  created_at: new Date().toISOString(),
@@ -78,6 +80,7 @@ class LeaveTypesResource {
78
80
  default_days: input.default_days,
79
81
  is_paid: input.is_paid,
80
82
  requires_approval: input.requires_approval,
83
+ requires_document: input.requires_document,
81
84
  active: input.active,
82
85
  last_updated_by: this.auth.mms_id,
83
86
  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
+ /** A `Date` at UTC midnight of a `YYYY-MM-DD` string (a longer ISO string is cut to its date). */
8
+ export declare const parseIsoDate: (value: string) => Date;
7
9
  /** The `YYYY-MM-DD` of a `Date`'s UTC calendar day. */
8
10
  export declare const toIsoDate: (date: Date) => string;
9
11
  /** Today's calendar date in `LEAVE_TIME_ZONE`. */
@@ -18,6 +20,8 @@ export declare const isWorkingDay: (isoDate: string, holidays: ReadonlySet<strin
18
20
  * on a weekend or holiday - the caller rejects a zero either way.
19
21
  */
20
22
  export declare const countWorkingDays: (start: string, end: string, holidays: ReadonlySet<string>, isHalfDay?: boolean) => number;
23
+ /** `isoDate` moved by `days` calendar days, either way; `shiftDays('2026-03-01', -1)` is Feb 28. */
24
+ export declare const shiftDays: (isoDate: string, days: number) => string;
21
25
  /** The day before the same date next year - the end of a period that starts on `isoDate`. */
22
26
  export declare const addOneYearMinusDay: (isoDate: string) => string;
23
27
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isWithinPeriod = exports.leavePeriodFor = exports.addOneYearMinusDay = exports.countWorkingDays = exports.isWorkingDay = exports.eachDate = exports.todayInJohannesburg = exports.toIsoDate = exports.LEAVE_TIME_ZONE = void 0;
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
4
  // The calendar arithmetic behind leave: which days of a request count, and which leave year a
5
5
  // date belongs to. 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.
@@ -17,6 +17,7 @@ const parseIsoDate = (value) => {
17
17
  const [year, month, day] = value.slice(0, 10).split('-').map(Number);
18
18
  return new Date(Date.UTC(year, month - 1, day));
19
19
  };
20
+ exports.parseIsoDate = parseIsoDate;
20
21
  /** The `YYYY-MM-DD` of a `Date`'s UTC calendar day. */
21
22
  const toIsoDate = (date) => `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
22
23
  exports.toIsoDate = toIsoDate;
@@ -34,14 +35,14 @@ exports.todayInJohannesburg = todayInJohannesburg;
34
35
  const eachDate = (start, end) => {
35
36
  const dates = [];
36
37
  const last = end.slice(0, 10);
37
- for (const cursor = parseIsoDate(start); (0, exports.toIsoDate)(cursor) <= last; cursor.setUTCDate(cursor.getUTCDate() + 1)) {
38
+ for (const cursor = (0, exports.parseIsoDate)(start); (0, exports.toIsoDate)(cursor) <= last; cursor.setUTCDate(cursor.getUTCDate() + 1)) {
38
39
  dates.push((0, exports.toIsoDate)(cursor));
39
40
  }
40
41
  return dates;
41
42
  };
42
43
  exports.eachDate = eachDate;
43
44
  /** Monday to Friday and not one of the organization's holidays. */
44
- const isWorkingDay = (isoDate, holidays) => !WEEKEND_DAYS.has(parseIsoDate(isoDate).getUTCDay()) && !holidays.has(isoDate.slice(0, 10));
45
+ const isWorkingDay = (isoDate, holidays) => !WEEKEND_DAYS.has((0, exports.parseIsoDate)(isoDate).getUTCDay()) && !holidays.has(isoDate.slice(0, 10));
45
46
  exports.isWorkingDay = isWorkingDay;
46
47
  /**
47
48
  * The days a request is worth: the working days from `start` to `end` inclusive, halved for a
@@ -53,9 +54,16 @@ const countWorkingDays = (start, end, holidays, isHalfDay = false) => {
53
54
  return isHalfDay ? working * 0.5 : working;
54
55
  };
55
56
  exports.countWorkingDays = countWorkingDays;
57
+ /** `isoDate` moved by `days` calendar days, either way; `shiftDays('2026-03-01', -1)` is Feb 28. */
58
+ const shiftDays = (isoDate, days) => {
59
+ const date = (0, exports.parseIsoDate)(isoDate);
60
+ date.setUTCDate(date.getUTCDate() + days);
61
+ return (0, exports.toIsoDate)(date);
62
+ };
63
+ exports.shiftDays = shiftDays;
56
64
  /** The day before the same date next year - the end of a period that starts on `isoDate`. */
57
65
  const addOneYearMinusDay = (isoDate) => {
58
- const date = parseIsoDate(isoDate);
66
+ const date = (0, exports.parseIsoDate)(isoDate);
59
67
  date.setUTCFullYear(date.getUTCFullYear() + 1);
60
68
  date.setUTCDate(date.getUTCDate() - 1);
61
69
  return (0, exports.toIsoDate)(date);
@@ -67,7 +75,7 @@ exports.addOneYearMinusDay = addOneYearMinusDay;
67
75
  * 2026-02-10 in the year that began 2025-03-01.
68
76
  */
69
77
  const leavePeriodFor = (date, startMonth) => {
70
- const parsed = parseIsoDate(date);
78
+ const parsed = (0, exports.parseIsoDate)(date);
71
79
  const year = parsed.getUTCMonth() + 1 >= startMonth ? parsed.getUTCFullYear() : parsed.getUTCFullYear() - 1;
72
80
  const period_start = (0, exports.toIsoDate)(new Date(Date.UTC(year, startMonth - 1, 1)));
73
81
  return { period_start, period_end: (0, exports.addOneYearMinusDay)(period_start) };
@@ -12,6 +12,10 @@ export declare class UpdatePermissionsDto implements UpdatePermissionsDtoType {
12
12
  readonly read_tasks: boolean;
13
13
  readonly update_tasks: boolean;
14
14
  readonly delete_tasks: boolean;
15
+ readonly create_timesheets: boolean;
16
+ readonly read_timesheets: boolean;
17
+ readonly update_timesheets: boolean;
18
+ readonly delete_timesheets: boolean;
15
19
  readonly create_leave: boolean;
16
20
  readonly read_leave: boolean;
17
21
  readonly update_leave: boolean;
@@ -34,6 +34,10 @@ class UpdatePermissionsDto {
34
34
  read_tasks;
35
35
  update_tasks;
36
36
  delete_tasks;
37
+ create_timesheets;
38
+ read_timesheets;
39
+ update_timesheets;
40
+ delete_timesheets;
37
41
  // @IsBoolean({ message: 'Create Documents must be a boolean' })
38
42
  // readonly create_documents!: boolean;
39
43
  // @IsBoolean({ message: 'Read Documents must be a boolean' })
@@ -152,6 +156,22 @@ __decorate([
152
156
  (0, class_validator_1.IsBoolean)({ message: 'Delete Tasks must be a boolean' }),
153
157
  __metadata("design:type", Boolean)
154
158
  ], UpdatePermissionsDto.prototype, "delete_tasks", void 0);
159
+ __decorate([
160
+ (0, class_validator_1.IsBoolean)({ message: 'Create Timesheets must be a boolean' }),
161
+ __metadata("design:type", Boolean)
162
+ ], UpdatePermissionsDto.prototype, "create_timesheets", void 0);
163
+ __decorate([
164
+ (0, class_validator_1.IsBoolean)({ message: 'Read Timesheets must be a boolean' }),
165
+ __metadata("design:type", Boolean)
166
+ ], UpdatePermissionsDto.prototype, "read_timesheets", void 0);
167
+ __decorate([
168
+ (0, class_validator_1.IsBoolean)({ message: 'Update Timesheets must be a boolean' }),
169
+ __metadata("design:type", Boolean)
170
+ ], UpdatePermissionsDto.prototype, "update_timesheets", void 0);
171
+ __decorate([
172
+ (0, class_validator_1.IsBoolean)({ message: 'Delete Timesheets must be a boolean' }),
173
+ __metadata("design:type", Boolean)
174
+ ], UpdatePermissionsDto.prototype, "delete_timesheets", void 0);
155
175
  __decorate([
156
176
  (0, class_validator_1.IsBoolean)({ message: 'Create Leave must be a boolean' }),
157
177
  __metadata("design:type", Boolean)