@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.
- package/README.md +75 -3
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +14 -1
- package/dist/leave/dto.d.ts +4 -0
- package/dist/leave/dto.js +24 -0
- package/dist/leave/leave-entitlements.d.ts +10 -3
- package/dist/leave/leave-entitlements.js +26 -6
- package/dist/leave/leave-requests.d.ts +2 -3
- package/dist/leave/leave-requests.js +2 -1
- package/dist/leave/leave-types.d.ts +2 -3
- package/dist/leave/leave-types.js +3 -0
- package/dist/leave/working-days.d.ts +4 -0
- package/dist/leave/working-days.js +13 -5
- package/dist/permissions/dto.d.ts +4 -0
- package/dist/permissions/dto.js +20 -0
- package/dist/service-client/index.d.ts +13 -0
- package/dist/service-client/index.js +22 -0
- package/dist/timesheets/dto.d.ts +42 -0
- package/dist/timesheets/dto.js +211 -0
- package/dist/timesheets/index.d.ts +4 -0
- package/dist/timesheets/index.js +27 -0
- package/dist/timesheets/timesheet-entries.d.ts +54 -0
- package/dist/timesheets/timesheet-entries.js +141 -0
- package/dist/timesheets/timesheets.d.ts +67 -0
- package/dist/timesheets/timesheets.js +201 -0
- package/dist/timesheets/weeks.d.ts +7 -0
- package/dist/timesheets/weeks.js +26 -0
- package/dist/transforms.d.ts +13 -0
- package/dist/transforms.js +24 -1
- package/dist/users/dto.d.ts +1 -0
- package/dist/users/dto.js +7 -0
- package/dist/users/index.d.ts +8 -1
- package/dist/users/index.js +17 -2
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +21 -1
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@ import { BillingResource } from '../billing';
|
|
|
4
4
|
import { InvoicesResource } from '../billing/invoices';
|
|
5
5
|
import { ConfigsResource } from '../configs';
|
|
6
6
|
import { FeatureRequestsResource } from '../feature-requests';
|
|
7
|
+
import { LeaveEntitlementsResource, LeaveRequestsResource, LeaveTypesResource } from '../leave';
|
|
7
8
|
import { NotificationsOutboxResource } from '../notifications';
|
|
8
9
|
import { OrganizationInvitesResource } from '../organization-invites';
|
|
9
10
|
import { OrganizationPaymentMethodsResource, type PaymentMethodLookupColumn } from '../organization-payment-methods';
|
|
@@ -30,6 +31,9 @@ export declare class SupabaseOrganizationScope {
|
|
|
30
31
|
readonly billing: BillingResource;
|
|
31
32
|
readonly invoices: InvoicesResource;
|
|
32
33
|
readonly invites: OrganizationInvitesResource;
|
|
34
|
+
readonly leaveTypes: LeaveTypesResource;
|
|
35
|
+
readonly leaveEntitlements: LeaveEntitlementsResource;
|
|
36
|
+
readonly leaveRequests: LeaveRequestsResource;
|
|
33
37
|
readonly organizations: OrganizationsResource;
|
|
34
38
|
readonly paymentMethod: OrganizationPaymentMethodsResource;
|
|
35
39
|
readonly permissions: PermissionsResource;
|
|
@@ -85,6 +89,15 @@ export declare class SupabaseServiceClient {
|
|
|
85
89
|
* Unscoped by nature: the cron has no tenant until it picks one.
|
|
86
90
|
*/
|
|
87
91
|
organizationsWithPaystackCustomer(): Promise<string[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Every organization whose leave year starts on the first of `month` (1-12) - the tenants the
|
|
94
|
+
* nightly rollover visits on that day. Unscoped like the reconcile: the cron picks its tenants
|
|
95
|
+
* from here. `modules` comes along so the caller can skip an organization without HR.
|
|
96
|
+
*/
|
|
97
|
+
organizationsWithLeaveYearStartingIn(month: number): Promise<{
|
|
98
|
+
mms_id: string;
|
|
99
|
+
modules: string[];
|
|
100
|
+
}[]>;
|
|
88
101
|
/**
|
|
89
102
|
* A ledger row by id alone. Also unscoped by necessity: a `charge.success` carries the
|
|
90
103
|
* transaction id in its metadata, and the row is what says which tenant it belongs to.
|
|
@@ -8,6 +8,7 @@ const billing_1 = require("../billing");
|
|
|
8
8
|
const invoices_1 = require("../billing/invoices");
|
|
9
9
|
const configs_1 = require("../configs");
|
|
10
10
|
const feature_requests_1 = require("../feature-requests");
|
|
11
|
+
const leave_1 = require("../leave");
|
|
11
12
|
const notifications_1 = require("../notifications");
|
|
12
13
|
const errors_1 = require("../errors");
|
|
13
14
|
const organization_invites_1 = require("../organization-invites");
|
|
@@ -28,6 +29,9 @@ class SupabaseOrganizationScope {
|
|
|
28
29
|
billing;
|
|
29
30
|
invoices;
|
|
30
31
|
invites;
|
|
32
|
+
leaveTypes;
|
|
33
|
+
leaveEntitlements;
|
|
34
|
+
leaveRequests;
|
|
31
35
|
organizations;
|
|
32
36
|
paymentMethod;
|
|
33
37
|
permissions;
|
|
@@ -39,6 +43,10 @@ class SupabaseOrganizationScope {
|
|
|
39
43
|
this.billing = new billing_1.BillingResource(supabase, actor, this.audit);
|
|
40
44
|
this.invoices = new invoices_1.InvoicesResource(supabase, actor, this.audit);
|
|
41
45
|
this.invites = new organization_invites_1.OrganizationInvitesResource(supabase, actor);
|
|
46
|
+
// The leave-year rollover writes next period's entitlements as the system (`mms_id: null`).
|
|
47
|
+
this.leaveTypes = new leave_1.LeaveTypesResource(supabase, actor, this.audit);
|
|
48
|
+
this.leaveEntitlements = new leave_1.LeaveEntitlementsResource(supabase, actor, this.audit);
|
|
49
|
+
this.leaveRequests = new leave_1.LeaveRequestsResource(supabase, actor, this.audit);
|
|
42
50
|
this.organizations = new organizations_1.OrganizationsResource(supabase, actor, this.audit);
|
|
43
51
|
this.paymentMethod = new organization_payment_methods_1.OrganizationPaymentMethodsResource(supabase, actor, this.audit);
|
|
44
52
|
this.permissions = new permissions_1.PermissionsResource(supabase, actor, this.audit);
|
|
@@ -146,6 +154,20 @@ class SupabaseServiceClient {
|
|
|
146
154
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
147
155
|
return (data ?? []).map((row) => row.organization_id);
|
|
148
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Every organization whose leave year starts on the first of `month` (1-12) - the tenants the
|
|
159
|
+
* nightly rollover visits on that day. Unscoped like the reconcile: the cron picks its tenants
|
|
160
|
+
* from here. `modules` comes along so the caller can skip an organization without HR.
|
|
161
|
+
*/
|
|
162
|
+
async organizationsWithLeaveYearStartingIn(month) {
|
|
163
|
+
const { data, error } = await this.supabase
|
|
164
|
+
.from('organizations')
|
|
165
|
+
.select('mms_id, modules')
|
|
166
|
+
.eq('leave_calendar_start_month', month);
|
|
167
|
+
if (error)
|
|
168
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
169
|
+
return (data ?? []);
|
|
170
|
+
}
|
|
149
171
|
/**
|
|
150
172
|
* A ledger row by id alone. Also unscoped by necessity: a `charge.success` carries the
|
|
151
173
|
* transaction id in its metadata, and the row is what says which tenant it belongs to.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CreateTimesheetEntryDto as CreateTimesheetEntryDtoType, GetTimesheetReportDto as GetTimesheetReportDtoType, GetTimesheetsDto as GetTimesheetsDtoType, ReviewTimesheetDto as ReviewTimesheetDtoType, TimesheetEntryIdDto as TimesheetEntryIdDtoType, TimesheetIdDto as TimesheetIdDtoType, UpdateTimesheetEntryDto as UpdateTimesheetEntryDtoType } from '@managemint-solutions/entities/timesheets/dto';
|
|
2
|
+
import { TimesheetReviewDecision, TimesheetStatus } from '@managemint-solutions/entities/timesheets/enum';
|
|
3
|
+
export declare class TimesheetIdDto implements TimesheetIdDtoType {
|
|
4
|
+
readonly timesheetId: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class GetTimesheetsDto implements GetTimesheetsDtoType {
|
|
7
|
+
readonly page: number;
|
|
8
|
+
readonly page_size: number;
|
|
9
|
+
readonly user_id?: string | null;
|
|
10
|
+
readonly status?: TimesheetStatus | null;
|
|
11
|
+
readonly week_start?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare class ReviewTimesheetDto implements ReviewTimesheetDtoType {
|
|
14
|
+
readonly status: TimesheetReviewDecision;
|
|
15
|
+
readonly review_note: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare class GetTimesheetReportDto implements GetTimesheetReportDtoType {
|
|
18
|
+
readonly date_from: string;
|
|
19
|
+
readonly date_to: string;
|
|
20
|
+
readonly user_id?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export declare class TimesheetEntryIdDto implements TimesheetEntryIdDtoType {
|
|
23
|
+
readonly timesheetEntryId: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class CreateTimesheetEntryDto implements CreateTimesheetEntryDtoType {
|
|
26
|
+
readonly date: string;
|
|
27
|
+
readonly start_time: string;
|
|
28
|
+
readonly end_time: string;
|
|
29
|
+
readonly client_id: string | null;
|
|
30
|
+
readonly project_id: string | null;
|
|
31
|
+
readonly task_id: string | null;
|
|
32
|
+
readonly description: string | null;
|
|
33
|
+
}
|
|
34
|
+
export declare class UpdateTimesheetEntryDto implements UpdateTimesheetEntryDtoType {
|
|
35
|
+
readonly date: string;
|
|
36
|
+
readonly start_time: string;
|
|
37
|
+
readonly end_time: string;
|
|
38
|
+
readonly client_id: string | null;
|
|
39
|
+
readonly project_id: string | null;
|
|
40
|
+
readonly task_id: string | null;
|
|
41
|
+
readonly description: string | null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UpdateTimesheetEntryDto = exports.CreateTimesheetEntryDto = exports.TimesheetEntryIdDto = exports.GetTimesheetReportDto = exports.ReviewTimesheetDto = exports.GetTimesheetsDto = exports.TimesheetIdDto = void 0;
|
|
13
|
+
const class_validator_1 = require("class-validator");
|
|
14
|
+
const class_transformer_1 = require("class-transformer");
|
|
15
|
+
const enum_1 = require("@managemint-solutions/entities/timesheets/enum");
|
|
16
|
+
const validators_1 = require("../validators");
|
|
17
|
+
// class-validator versions of the shared DTO types from @managemint-solutions/entities.
|
|
18
|
+
// Each class `implements` its entities type so the wire contract cannot drift.
|
|
19
|
+
//
|
|
20
|
+
// Body fields are required: a nullable one carries `@IsNullable()` without `@IsOptional()`,
|
|
21
|
+
// so `null` passes and a missing key fails. Only the query-string filters are optional.
|
|
22
|
+
/** A description is stored trimmed; one that is blank once trimmed means "none". Goes above `EmptyToNull()`, which runs after it. */
|
|
23
|
+
const Trim = () => (0, class_transformer_1.Transform)(({ value }) => (typeof value === 'string' ? value.trim() : value), { toClassOnly: true });
|
|
24
|
+
// `user_id` is v4 (the GoTrue auth uid, see users/dto.ts); every other id is v7.
|
|
25
|
+
// ---- Timesheets (weeks) ----
|
|
26
|
+
class TimesheetIdDto {
|
|
27
|
+
timesheetId;
|
|
28
|
+
}
|
|
29
|
+
exports.TimesheetIdDto = TimesheetIdDto;
|
|
30
|
+
__decorate([
|
|
31
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid timesheet ID format' }),
|
|
32
|
+
__metadata("design:type", String)
|
|
33
|
+
], TimesheetIdDto.prototype, "timesheetId", void 0);
|
|
34
|
+
class GetTimesheetsDto {
|
|
35
|
+
page = 1;
|
|
36
|
+
page_size = 10;
|
|
37
|
+
user_id;
|
|
38
|
+
status;
|
|
39
|
+
week_start;
|
|
40
|
+
}
|
|
41
|
+
exports.GetTimesheetsDto = GetTimesheetsDto;
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, class_validator_1.IsOptional)(),
|
|
44
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 1, { toClassOnly: true }),
|
|
45
|
+
(0, class_validator_1.IsNumber)({}, { message: 'Page must be a number' }),
|
|
46
|
+
(0, class_validator_1.Min)(1, { message: 'Page must be at least 1' }),
|
|
47
|
+
__metadata("design:type", Number)
|
|
48
|
+
], GetTimesheetsDto.prototype, "page", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
(0, class_validator_1.IsOptional)(),
|
|
51
|
+
(0, class_transformer_1.Transform)(({ value }) => value ? Number(value) : 10, { toClassOnly: true }),
|
|
52
|
+
(0, class_validator_1.IsNumber)({}, { message: 'Page size must be a number' }),
|
|
53
|
+
(0, class_validator_1.Min)(1, { message: 'Page size must be at least 1' }),
|
|
54
|
+
(0, class_validator_1.Max)(100, { message: 'Page size cannot exceed 100' }),
|
|
55
|
+
__metadata("design:type", Number)
|
|
56
|
+
], GetTimesheetsDto.prototype, "page_size", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
(0, class_validator_1.IsOptional)(),
|
|
59
|
+
(0, validators_1.IsNullable)(),
|
|
60
|
+
(0, class_validator_1.IsUUID)('4', { message: 'Invalid user ID format' }),
|
|
61
|
+
__metadata("design:type", Object)
|
|
62
|
+
], GetTimesheetsDto.prototype, "user_id", void 0);
|
|
63
|
+
__decorate([
|
|
64
|
+
(0, class_validator_1.IsOptional)(),
|
|
65
|
+
(0, validators_1.IsNullable)(),
|
|
66
|
+
(0, class_validator_1.IsEnum)(enum_1.TimesheetStatus, { message: 'Invalid status' }),
|
|
67
|
+
__metadata("design:type", Object)
|
|
68
|
+
], GetTimesheetsDto.prototype, "status", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
(0, class_validator_1.IsOptional)(),
|
|
71
|
+
(0, validators_1.IsNullable)(),
|
|
72
|
+
(0, validators_1.IsCalendarDate)({ message: 'Week start must be a calendar date (YYYY-MM-DD)' }),
|
|
73
|
+
__metadata("design:type", Object)
|
|
74
|
+
], GetTimesheetsDto.prototype, "week_start", void 0);
|
|
75
|
+
class ReviewTimesheetDto {
|
|
76
|
+
status;
|
|
77
|
+
review_note;
|
|
78
|
+
}
|
|
79
|
+
exports.ReviewTimesheetDto = ReviewTimesheetDto;
|
|
80
|
+
__decorate([
|
|
81
|
+
(0, class_validator_1.IsEnum)(enum_1.TimesheetReviewDecision, { message: 'Status must be approved or rejected' }),
|
|
82
|
+
__metadata("design:type", String)
|
|
83
|
+
], ReviewTimesheetDto.prototype, "status", void 0);
|
|
84
|
+
__decorate([
|
|
85
|
+
(0, validators_1.EmptyToNull)(),
|
|
86
|
+
(0, validators_1.IsNullable)(),
|
|
87
|
+
(0, class_validator_1.IsString)({ message: 'Invalid review note' }),
|
|
88
|
+
(0, class_validator_1.MaxLength)(2000, { message: 'Review note cannot exceed 2000 characters' }),
|
|
89
|
+
__metadata("design:type", Object)
|
|
90
|
+
], ReviewTimesheetDto.prototype, "review_note", void 0);
|
|
91
|
+
class GetTimesheetReportDto {
|
|
92
|
+
date_from;
|
|
93
|
+
date_to;
|
|
94
|
+
user_id;
|
|
95
|
+
}
|
|
96
|
+
exports.GetTimesheetReportDto = GetTimesheetReportDto;
|
|
97
|
+
__decorate([
|
|
98
|
+
(0, validators_1.IsCalendarDate)({ message: 'Date from must be a calendar date (YYYY-MM-DD)' }),
|
|
99
|
+
__metadata("design:type", String)
|
|
100
|
+
], GetTimesheetReportDto.prototype, "date_from", void 0);
|
|
101
|
+
__decorate([
|
|
102
|
+
(0, validators_1.IsCalendarDate)({ message: 'Date to must be a calendar date (YYYY-MM-DD)' }),
|
|
103
|
+
__metadata("design:type", String)
|
|
104
|
+
], GetTimesheetReportDto.prototype, "date_to", void 0);
|
|
105
|
+
__decorate([
|
|
106
|
+
(0, class_validator_1.IsOptional)(),
|
|
107
|
+
(0, validators_1.IsNullable)(),
|
|
108
|
+
(0, class_validator_1.IsUUID)('4', { message: 'Invalid user ID format' }),
|
|
109
|
+
__metadata("design:type", Object)
|
|
110
|
+
], GetTimesheetReportDto.prototype, "user_id", void 0);
|
|
111
|
+
// ---- Timesheet entries ----
|
|
112
|
+
class TimesheetEntryIdDto {
|
|
113
|
+
timesheetEntryId;
|
|
114
|
+
}
|
|
115
|
+
exports.TimesheetEntryIdDto = TimesheetEntryIdDto;
|
|
116
|
+
__decorate([
|
|
117
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid timesheet entry ID format' }),
|
|
118
|
+
__metadata("design:type", String)
|
|
119
|
+
], TimesheetEntryIdDto.prototype, "timesheetEntryId", void 0);
|
|
120
|
+
// No `user_id`: an entry is always the caller's own, and leaving the field out is what keeps a
|
|
121
|
+
// forged one from validating. The week is found or created from `date` by the api.
|
|
122
|
+
class CreateTimesheetEntryDto {
|
|
123
|
+
date;
|
|
124
|
+
start_time;
|
|
125
|
+
end_time;
|
|
126
|
+
client_id;
|
|
127
|
+
project_id;
|
|
128
|
+
task_id;
|
|
129
|
+
description;
|
|
130
|
+
}
|
|
131
|
+
exports.CreateTimesheetEntryDto = CreateTimesheetEntryDto;
|
|
132
|
+
__decorate([
|
|
133
|
+
(0, validators_1.IsCalendarDate)({ message: 'Date must be a calendar date (YYYY-MM-DD)' }),
|
|
134
|
+
__metadata("design:type", String)
|
|
135
|
+
], CreateTimesheetEntryDto.prototype, "date", void 0);
|
|
136
|
+
__decorate([
|
|
137
|
+
(0, validators_1.IsClockTime)({ message: 'Start time must be a clock time (HH:MM)' }),
|
|
138
|
+
__metadata("design:type", String)
|
|
139
|
+
], CreateTimesheetEntryDto.prototype, "start_time", void 0);
|
|
140
|
+
__decorate([
|
|
141
|
+
(0, validators_1.IsClockTime)({ allowEndOfDay: true, message: 'End time must be a clock time (HH:MM, or 24:00 for midnight)' }),
|
|
142
|
+
__metadata("design:type", String)
|
|
143
|
+
], CreateTimesheetEntryDto.prototype, "end_time", void 0);
|
|
144
|
+
__decorate([
|
|
145
|
+
(0, validators_1.IsNullable)(),
|
|
146
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid client ID format' }),
|
|
147
|
+
__metadata("design:type", Object)
|
|
148
|
+
], CreateTimesheetEntryDto.prototype, "client_id", void 0);
|
|
149
|
+
__decorate([
|
|
150
|
+
(0, validators_1.IsNullable)(),
|
|
151
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid project ID format' }),
|
|
152
|
+
__metadata("design:type", Object)
|
|
153
|
+
], CreateTimesheetEntryDto.prototype, "project_id", void 0);
|
|
154
|
+
__decorate([
|
|
155
|
+
(0, validators_1.IsNullable)(),
|
|
156
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid task ID format' }),
|
|
157
|
+
__metadata("design:type", Object)
|
|
158
|
+
], CreateTimesheetEntryDto.prototype, "task_id", void 0);
|
|
159
|
+
__decorate([
|
|
160
|
+
Trim(),
|
|
161
|
+
(0, validators_1.EmptyToNull)(),
|
|
162
|
+
(0, validators_1.IsNullable)(),
|
|
163
|
+
(0, class_validator_1.IsString)({ message: 'Invalid description' }),
|
|
164
|
+
(0, class_validator_1.MaxLength)(2000, { message: 'Description cannot exceed 2000 characters' }),
|
|
165
|
+
__metadata("design:type", Object)
|
|
166
|
+
], CreateTimesheetEntryDto.prototype, "description", void 0);
|
|
167
|
+
class UpdateTimesheetEntryDto {
|
|
168
|
+
date;
|
|
169
|
+
start_time;
|
|
170
|
+
end_time;
|
|
171
|
+
client_id;
|
|
172
|
+
project_id;
|
|
173
|
+
task_id;
|
|
174
|
+
description;
|
|
175
|
+
}
|
|
176
|
+
exports.UpdateTimesheetEntryDto = UpdateTimesheetEntryDto;
|
|
177
|
+
__decorate([
|
|
178
|
+
(0, validators_1.IsCalendarDate)({ message: 'Date must be a calendar date (YYYY-MM-DD)' }),
|
|
179
|
+
__metadata("design:type", String)
|
|
180
|
+
], UpdateTimesheetEntryDto.prototype, "date", void 0);
|
|
181
|
+
__decorate([
|
|
182
|
+
(0, validators_1.IsClockTime)({ message: 'Start time must be a clock time (HH:MM)' }),
|
|
183
|
+
__metadata("design:type", String)
|
|
184
|
+
], UpdateTimesheetEntryDto.prototype, "start_time", void 0);
|
|
185
|
+
__decorate([
|
|
186
|
+
(0, validators_1.IsClockTime)({ allowEndOfDay: true, message: 'End time must be a clock time (HH:MM, or 24:00 for midnight)' }),
|
|
187
|
+
__metadata("design:type", String)
|
|
188
|
+
], UpdateTimesheetEntryDto.prototype, "end_time", void 0);
|
|
189
|
+
__decorate([
|
|
190
|
+
(0, validators_1.IsNullable)(),
|
|
191
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid client ID format' }),
|
|
192
|
+
__metadata("design:type", Object)
|
|
193
|
+
], UpdateTimesheetEntryDto.prototype, "client_id", void 0);
|
|
194
|
+
__decorate([
|
|
195
|
+
(0, validators_1.IsNullable)(),
|
|
196
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid project ID format' }),
|
|
197
|
+
__metadata("design:type", Object)
|
|
198
|
+
], UpdateTimesheetEntryDto.prototype, "project_id", void 0);
|
|
199
|
+
__decorate([
|
|
200
|
+
(0, validators_1.IsNullable)(),
|
|
201
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid task ID format' }),
|
|
202
|
+
__metadata("design:type", Object)
|
|
203
|
+
], UpdateTimesheetEntryDto.prototype, "task_id", void 0);
|
|
204
|
+
__decorate([
|
|
205
|
+
Trim(),
|
|
206
|
+
(0, validators_1.EmptyToNull)(),
|
|
207
|
+
(0, validators_1.IsNullable)(),
|
|
208
|
+
(0, class_validator_1.IsString)({ message: 'Invalid description' }),
|
|
209
|
+
(0, class_validator_1.MaxLength)(2000, { message: 'Description cannot exceed 2000 characters' }),
|
|
210
|
+
__metadata("design:type", Object)
|
|
211
|
+
], UpdateTimesheetEntryDto.prototype, "description", void 0);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './dto';
|
|
2
|
+
export * from './weeks';
|
|
3
|
+
export { TimesheetsResource, type CreateTimesheetRow, type ReviewTimesheetRow, type TimesheetCover, } from './timesheets';
|
|
4
|
+
export { TimesheetEntriesResource, type CreateTimesheetEntryRow, type TimesheetReportRow, type UpdateTimesheetEntryRow, } from './timesheet-entries';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Timesheets, the second slice of the HR module: the entries an employee logs, the
|
|
3
|
+
// Monday-to-Sunday weeks they submit, the DTOs the api validates with, and the week arithmetic
|
|
4
|
+
// it derives the week from. The resources hang off `SupabaseClient` as `timesheets` and
|
|
5
|
+
// `timesheetEntries`; the report itself is aggregated in the api from `forReport` rows.
|
|
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.TimesheetEntriesResource = exports.TimesheetsResource = void 0;
|
|
22
|
+
__exportStar(require("./dto"), exports);
|
|
23
|
+
__exportStar(require("./weeks"), exports);
|
|
24
|
+
var timesheets_1 = require("./timesheets");
|
|
25
|
+
Object.defineProperty(exports, "TimesheetsResource", { enumerable: true, get: function () { return timesheets_1.TimesheetsResource; } });
|
|
26
|
+
var timesheet_entries_1 = require("./timesheet-entries");
|
|
27
|
+
Object.defineProperty(exports, "TimesheetEntriesResource", { enumerable: true, get: function () { return timesheet_entries_1.TimesheetEntriesResource; } });
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { TimesheetEntryEntity } from '@managemint-solutions/entities/timesheets';
|
|
2
|
+
import type { AuthContext } from '../auth';
|
|
3
|
+
import { type AuditResource } from '../audit';
|
|
4
|
+
import type { TypedSupabaseClient } from '../client';
|
|
5
|
+
export declare const TIMESHEET_ENTRY_SELECT: "\n mms_id,\n created_at,\n created_by:user_profiles!created_by (*),\n updated_at,\n last_updated_by:user_profiles!last_updated_by (*),\n timesheet_id,\n user:user_profiles!user_id (*),\n date,\n start_time,\n end_time,\n duration_minutes,\n client:client_profiles!client_id (*),\n project:project_profiles!project_id (*),\n task:task_profiles!task_id (*),\n description\n ";
|
|
6
|
+
/** An entry as the api inserts it: the caller's own, in the week the api found or created for `date`. */
|
|
7
|
+
export type CreateTimesheetEntryRow = {
|
|
8
|
+
timesheet_id: string;
|
|
9
|
+
user_id: string;
|
|
10
|
+
date: string;
|
|
11
|
+
start_time: string;
|
|
12
|
+
end_time: string;
|
|
13
|
+
client_id: string | null;
|
|
14
|
+
project_id: string | null;
|
|
15
|
+
task_id: string | null;
|
|
16
|
+
description: string | null;
|
|
17
|
+
};
|
|
18
|
+
/** The editable fields; `timesheet_id` moves with the date when the api re-resolves the week. */
|
|
19
|
+
export type UpdateTimesheetEntryRow = Omit<CreateTimesheetEntryRow, 'user_id'>;
|
|
20
|
+
/** One entry as the report aggregates it: what was logged and who it is attributed to, without the audit stamps. */
|
|
21
|
+
export type TimesheetReportRow = Pick<TimesheetEntryEntity, 'timesheet_id' | 'user' | 'date' | 'start_time' | 'end_time' | 'duration_minutes' | 'client' | 'project' | 'task' | 'description'>;
|
|
22
|
+
/**
|
|
23
|
+
* Postgres returns a `time` as `HH:MM:SS`; the contract is `HH:MM`, the shape the DTOs accept,
|
|
24
|
+
* so a row read back can be sent straight back in. The one place a time is shaped on read.
|
|
25
|
+
*/
|
|
26
|
+
export declare const withClockTimes: <T extends {
|
|
27
|
+
start_time: string;
|
|
28
|
+
end_time: string;
|
|
29
|
+
duration_minutes: number | string;
|
|
30
|
+
}>(row: T) => T;
|
|
31
|
+
export declare class TimesheetEntriesResource {
|
|
32
|
+
private readonly supabase;
|
|
33
|
+
private readonly auth;
|
|
34
|
+
private readonly table;
|
|
35
|
+
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
|
|
36
|
+
getById(timesheetEntryId: string): Promise<TimesheetEntryEntity>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether `userId` already has an entry on `date` sharing a minute with `start`..`end` (two
|
|
39
|
+
* entries may touch: one ending 12:00 and one starting 12:00 do not overlap). The readable
|
|
40
|
+
* half of the rule; the table's exclusion constraint is the half that survives a race.
|
|
41
|
+
* `excludeId` leaves out the entry being edited.
|
|
42
|
+
*/
|
|
43
|
+
overlaps(userId: string, date: string, start: string, end: string, excludeId?: string): Promise<boolean>;
|
|
44
|
+
create(input: CreateTimesheetEntryRow): Promise<TimesheetEntryEntity>;
|
|
45
|
+
update(timesheetEntryId: string, input: UpdateTimesheetEntryRow): Promise<TimesheetEntryEntity>;
|
|
46
|
+
/** Hard delete; the week's totals are counted on read, so nothing else moves. */
|
|
47
|
+
remove(timesheetEntryId: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Every entry of `userIds` dated `dateFrom`..`dateTo`, flat, for the api to aggregate into the
|
|
50
|
+
* report - it keeps the ones whose `timesheet_id` is in `timesheets.approvedIds`. Paged, since
|
|
51
|
+
* a year of a team's entries outgrows one response.
|
|
52
|
+
*/
|
|
53
|
+
forReport(userIds: string[], dateFrom: string, dateTo: string): Promise<TimesheetReportRow[]>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimesheetEntriesResource = exports.withClockTimes = exports.TIMESHEET_ENTRY_SELECT = 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
|
+
const transforms_1 = require("../transforms");
|
|
8
|
+
// The public contract is the shared TimesheetEntryEntity from @managemint-solutions/entities;
|
|
9
|
+
// the casts at the returns declare the shape the select string produces (the columns are
|
|
10
|
+
// defined in the api's supabase/migrations). The attribution embeds go through the same
|
|
11
|
+
// `*_profiles` views the tasks resource uses - `(mms_id, name)`, the identity shapes.
|
|
12
|
+
exports.TIMESHEET_ENTRY_SELECT = `
|
|
13
|
+
mms_id,
|
|
14
|
+
created_at,
|
|
15
|
+
created_by:user_profiles!created_by (*),
|
|
16
|
+
updated_at,
|
|
17
|
+
last_updated_by:user_profiles!last_updated_by (*),
|
|
18
|
+
timesheet_id,
|
|
19
|
+
user:user_profiles!user_id (*),
|
|
20
|
+
date,
|
|
21
|
+
start_time,
|
|
22
|
+
end_time,
|
|
23
|
+
duration_minutes,
|
|
24
|
+
client:client_profiles!client_id (*),
|
|
25
|
+
project:project_profiles!project_id (*),
|
|
26
|
+
task:task_profiles!task_id (*),
|
|
27
|
+
description
|
|
28
|
+
`;
|
|
29
|
+
const REPORT_SELECT = `
|
|
30
|
+
timesheet_id,
|
|
31
|
+
user:user_profiles!user_id (*),
|
|
32
|
+
date,
|
|
33
|
+
start_time,
|
|
34
|
+
end_time,
|
|
35
|
+
duration_minutes,
|
|
36
|
+
client:client_profiles!client_id (*),
|
|
37
|
+
project:project_profiles!project_id (*),
|
|
38
|
+
task:task_profiles!task_id (*),
|
|
39
|
+
description
|
|
40
|
+
`;
|
|
41
|
+
/**
|
|
42
|
+
* Postgres returns a `time` as `HH:MM:SS`; the contract is `HH:MM`, the shape the DTOs accept,
|
|
43
|
+
* so a row read back can be sent straight back in. The one place a time is shaped on read.
|
|
44
|
+
*/
|
|
45
|
+
const withClockTimes = (row) => ({
|
|
46
|
+
...row,
|
|
47
|
+
start_time: row.start_time.slice(0, 5),
|
|
48
|
+
end_time: row.end_time.slice(0, 5),
|
|
49
|
+
duration_minutes: Number(row.duration_minutes),
|
|
50
|
+
});
|
|
51
|
+
exports.withClockTimes = withClockTimes;
|
|
52
|
+
class TimesheetEntriesResource {
|
|
53
|
+
supabase;
|
|
54
|
+
auth;
|
|
55
|
+
table;
|
|
56
|
+
constructor(supabase, auth, audit) {
|
|
57
|
+
this.supabase = supabase;
|
|
58
|
+
this.auth = auth;
|
|
59
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
60
|
+
table: 'timesheet_entries',
|
|
61
|
+
entityType: enum_1.AuditTrailEntityType.TIMESHEET_ENTRIES,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async getById(timesheetEntryId) {
|
|
65
|
+
const { data, error } = await this.supabase
|
|
66
|
+
.from('timesheet_entries')
|
|
67
|
+
.select(exports.TIMESHEET_ENTRY_SELECT)
|
|
68
|
+
.eq('mms_id', timesheetEntryId)
|
|
69
|
+
.eq('organization_id', this.auth.organization_id)
|
|
70
|
+
.single();
|
|
71
|
+
if (error)
|
|
72
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
73
|
+
return (0, exports.withClockTimes)(data);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether `userId` already has an entry on `date` sharing a minute with `start`..`end` (two
|
|
77
|
+
* entries may touch: one ending 12:00 and one starting 12:00 do not overlap). The readable
|
|
78
|
+
* half of the rule; the table's exclusion constraint is the half that survives a race.
|
|
79
|
+
* `excludeId` leaves out the entry being edited.
|
|
80
|
+
*/
|
|
81
|
+
async overlaps(userId, date, start, end, excludeId) {
|
|
82
|
+
let overlapQuery = this.supabase
|
|
83
|
+
.from('timesheet_entries')
|
|
84
|
+
.select('mms_id', { count: 'exact', head: true })
|
|
85
|
+
.eq('organization_id', this.auth.organization_id)
|
|
86
|
+
.eq('user_id', userId)
|
|
87
|
+
.eq('date', date)
|
|
88
|
+
.lt('start_time', end)
|
|
89
|
+
.gt('end_time', start);
|
|
90
|
+
if (excludeId) {
|
|
91
|
+
overlapQuery = overlapQuery.neq('mms_id', excludeId);
|
|
92
|
+
}
|
|
93
|
+
const { count, error } = await overlapQuery;
|
|
94
|
+
if (error)
|
|
95
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
96
|
+
return (count ?? 0) > 0;
|
|
97
|
+
}
|
|
98
|
+
async create(input) {
|
|
99
|
+
const row = await this.table.insert({
|
|
100
|
+
...input,
|
|
101
|
+
organization_id: this.auth.organization_id,
|
|
102
|
+
created_by: this.auth.mms_id,
|
|
103
|
+
created_at: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
return this.getById(row.mms_id);
|
|
106
|
+
}
|
|
107
|
+
async update(timesheetEntryId, input) {
|
|
108
|
+
await this.table.update(timesheetEntryId, {
|
|
109
|
+
...input,
|
|
110
|
+
last_updated_by: this.auth.mms_id,
|
|
111
|
+
updated_at: new Date().toISOString(),
|
|
112
|
+
});
|
|
113
|
+
return this.getById(timesheetEntryId);
|
|
114
|
+
}
|
|
115
|
+
/** Hard delete; the week's totals are counted on read, so nothing else moves. */
|
|
116
|
+
async remove(timesheetEntryId) {
|
|
117
|
+
await this.table.remove(timesheetEntryId);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Every entry of `userIds` dated `dateFrom`..`dateTo`, flat, for the api to aggregate into the
|
|
121
|
+
* report - it keeps the ones whose `timesheet_id` is in `timesheets.approvedIds`. Paged, since
|
|
122
|
+
* a year of a team's entries outgrows one response.
|
|
123
|
+
*/
|
|
124
|
+
async forReport(userIds, dateFrom, dateTo) {
|
|
125
|
+
if (userIds.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
const rows = await (0, transforms_1.allRows)((from, to) => this.supabase
|
|
128
|
+
.from('timesheet_entries')
|
|
129
|
+
.select(REPORT_SELECT)
|
|
130
|
+
.eq('organization_id', this.auth.organization_id)
|
|
131
|
+
.in('user_id', userIds)
|
|
132
|
+
.gte('date', dateFrom)
|
|
133
|
+
.lte('date', dateTo)
|
|
134
|
+
.order('date')
|
|
135
|
+
.order('start_time')
|
|
136
|
+
.order('mms_id')
|
|
137
|
+
.range(from, to));
|
|
138
|
+
return rows.map(exports.withClockTimes);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.TimesheetEntriesResource = TimesheetEntriesResource;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
|
+
import type { TimesheetEntity, TimesheetSummary } from '@managemint-solutions/entities/timesheets';
|
|
3
|
+
import { TimesheetReviewDecision, TimesheetStatus } from '@managemint-solutions/entities/timesheets/enum';
|
|
4
|
+
import type { AuthContext } from '../auth';
|
|
5
|
+
import { type AuditResource } from '../audit';
|
|
6
|
+
import type { TypedSupabaseClient } from '../client';
|
|
7
|
+
import type { GetTimesheetsDto } from './dto';
|
|
8
|
+
/** What the entry path needs of the week an entry falls in: its id and whether it is still open. */
|
|
9
|
+
export type TimesheetCover = {
|
|
10
|
+
mms_id: string;
|
|
11
|
+
status: TimesheetStatus;
|
|
12
|
+
week_start: string;
|
|
13
|
+
};
|
|
14
|
+
/** A week as the api creates it - as a draft, for the first entry dated inside it. */
|
|
15
|
+
export type CreateTimesheetRow = {
|
|
16
|
+
user_id: string;
|
|
17
|
+
week_start: string;
|
|
18
|
+
};
|
|
19
|
+
export type ReviewTimesheetRow = {
|
|
20
|
+
status: TimesheetReviewDecision;
|
|
21
|
+
reviewed_by: string;
|
|
22
|
+
review_note: string | null;
|
|
23
|
+
};
|
|
24
|
+
/** The list's visibility: `userIds` is what the api resolved for the caller. */
|
|
25
|
+
type ListOptions = {
|
|
26
|
+
userIds?: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare class TimesheetsResource {
|
|
29
|
+
private readonly supabase;
|
|
30
|
+
private readonly auth;
|
|
31
|
+
private readonly table;
|
|
32
|
+
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
|
|
33
|
+
list(query: GetTimesheetsDto, options?: ListOptions): Promise<MmsList<TimesheetSummary>>;
|
|
34
|
+
/** The week with its entries, by date then start time. */
|
|
35
|
+
getById(timesheetId: string): Promise<TimesheetEntity>;
|
|
36
|
+
/**
|
|
37
|
+
* The week of `userId` starting on `weekStart`, or null when no entry has opened it yet. One
|
|
38
|
+
* row per user and Monday (the table's unique key), so there is at most one.
|
|
39
|
+
*/
|
|
40
|
+
findFor(userId: string, weekStart: string): Promise<TimesheetCover | null>;
|
|
41
|
+
/** Opens a week as a draft. A second opener of the same week hits the unique key - the SDK's 409. */
|
|
42
|
+
create(input: CreateTimesheetRow): Promise<TimesheetSummary>;
|
|
43
|
+
/**
|
|
44
|
+
* Sends a draft or rejected week for review, clearing the previous decision. Resolves to null
|
|
45
|
+
* when the week was no longer in either state - submitted twice from two tabs, say.
|
|
46
|
+
*/
|
|
47
|
+
submit(timesheetId: string): Promise<TimesheetSummary | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Approves or rejects a submitted week. Resolves to null when the week was no longer
|
|
50
|
+
* submitted - another reviewer got there first - so the caller can say so instead of
|
|
51
|
+
* overwriting their decision.
|
|
52
|
+
*/
|
|
53
|
+
review(timesheetId: string, input: ReviewTimesheetRow): Promise<TimesheetSummary | null>;
|
|
54
|
+
/**
|
|
55
|
+
* The ids of `userIds`' approved weeks that could hold an entry dated `dateFrom`..`dateTo` -
|
|
56
|
+
* the weeks starting from the Monday of `dateFrom` up to `dateTo`. The report keeps only the
|
|
57
|
+
* entries of these weeks.
|
|
58
|
+
*/
|
|
59
|
+
approvedIds(userIds: string[], dateFrom: string, dateTo: string): Promise<Set<string>>;
|
|
60
|
+
private summaryById;
|
|
61
|
+
/**
|
|
62
|
+
* Adds `week_end` and the totals to each row from one query over the entries of these weeks.
|
|
63
|
+
* Nothing is stored: a total is always what the entries add up to.
|
|
64
|
+
*/
|
|
65
|
+
private withTotals;
|
|
66
|
+
}
|
|
67
|
+
export {};
|