@fabriktor/fx 0.0.114 → 0.0.116
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/dist/src/billable/billable.d.ts +4 -11
- package/dist/src/billable/billable.js +10 -65
- package/dist/src/contacts/contacts.js +4 -12
- package/dist/src/contacts/scope.d.ts +14 -0
- package/dist/src/contacts/scope.js +49 -0
- package/dist/src/core/id.d.ts +1 -0
- package/dist/src/core/id.js +9 -0
- package/dist/src/core/query.d.ts +9 -0
- package/dist/src/core/query.js +15 -0
- package/dist/src/core/scope.d.ts +5 -0
- package/dist/src/core/scope.js +23 -0
- package/dist/src/core/search.d.ts +4 -7
- package/dist/src/core/search.js +13 -23
- package/dist/src/fx.js +2 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/jobs/jobs.js +1 -14
- package/dist/src/timesheets/access.d.ts +27 -0
- package/dist/src/timesheets/access.js +48 -0
- package/dist/src/timesheets/timesheets.d.ts +18 -1
- package/dist/src/timesheets/timesheets.js +53 -22
- package/dist/src/timesheets/workspace.d.ts +79 -0
- package/dist/src/timesheets/workspace.js +331 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type BillableOperator } from "@fabriktor/client";
|
|
2
2
|
import { type Bill, BillableFolder, Charge, Equipment, Item, LaborRate, MeterReading, Quantifier, Tax, TmpBillNumber, VendorInvoice, Warehouse } from "@fabriktor/schema";
|
|
3
|
-
import type { ContactsFXOperator,
|
|
3
|
+
import type { ContactsFXOperator, EmploymentOwnerScopeOptions } from "../contacts/contacts.js";
|
|
4
|
+
import { type EmploymentReadContext } from "../contacts/scope.js";
|
|
4
5
|
import type { MultiCreateCheckpoint } from "../core/multi-create.js";
|
|
5
6
|
import { type OwnerSearchOptions, type SearchOptions, type SearchPage, type SearchQuery } from "../core/search.js";
|
|
6
7
|
type GenerateBillPayload = Parameters<BillableOperator["generateBill"]>[0];
|
|
@@ -98,16 +99,8 @@ export type BillableFXOptions = {
|
|
|
98
99
|
billable: BillableOperator;
|
|
99
100
|
contacts_fx: ContactsFXOperator;
|
|
100
101
|
};
|
|
101
|
-
export type LoadBillableReadContextOptions =
|
|
102
|
-
|
|
103
|
-
uid: string;
|
|
104
|
-
active_mode: string;
|
|
105
|
-
};
|
|
106
|
-
export type BillableReadContext = {
|
|
107
|
-
owner_id: string;
|
|
108
|
-
active_mode: string;
|
|
109
|
-
employment: EmploymentOwnerScope;
|
|
110
|
-
};
|
|
102
|
+
export type LoadBillableReadContextOptions = EmploymentOwnerScopeOptions;
|
|
103
|
+
export type BillableReadContext = EmploymentReadContext;
|
|
111
104
|
export declare const BillableReadResource: {
|
|
112
105
|
readonly Bill: "bill";
|
|
113
106
|
readonly BillableFolder: "billable_folder";
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
6
|
import { mergeQueryOptions, queryNestedParam, queryParams, } from "@fabriktor/client";
|
|
7
7
|
import { BillTypeDynamic, BillTypeFixed, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
|
|
8
|
+
import { employmentActivityOwnerIDs, employmentCatalogOwnerIDs as catalogOwnerIDs, employmentEmployerIDs, employmentManagesRecord, employmentOwnsRecord, loadEmploymentReadContext, } from "../contacts/scope.js";
|
|
9
|
+
import { restrictOwnerIDs } from "../core/scope.js";
|
|
8
10
|
import { isZeroDateTime } from "../core/date.js";
|
|
9
11
|
import { errRequired, errResolveFailed, errValidation } from "../core/err.js";
|
|
10
12
|
import { requiredOperationID, resolvedID } from "../core/id.js";
|
|
@@ -109,23 +111,7 @@ export class BillableFX {
|
|
|
109
111
|
this.contacts_fx = opts.contacts_fx;
|
|
110
112
|
}
|
|
111
113
|
async loadReadContext(opts) {
|
|
112
|
-
|
|
113
|
-
if (owner_id === "") {
|
|
114
|
-
throw errRequired({
|
|
115
|
-
field: "owner_id",
|
|
116
|
-
msg: msgOwnerIDRequired,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
const employment = await this.contacts_fx.employmentOwnerScope({
|
|
120
|
-
owner_id,
|
|
121
|
-
uid: opts.uid,
|
|
122
|
-
active_mode: opts.active_mode,
|
|
123
|
-
});
|
|
124
|
-
return {
|
|
125
|
-
owner_id,
|
|
126
|
-
active_mode: opts.active_mode,
|
|
127
|
-
employment,
|
|
128
|
-
};
|
|
114
|
+
return await loadEmploymentReadContext(this.contacts_fx, opts);
|
|
129
115
|
}
|
|
130
116
|
async searchBills(opts) {
|
|
131
117
|
return await runOwnerSearch({
|
|
@@ -175,12 +161,12 @@ export class BillableFX {
|
|
|
175
161
|
});
|
|
176
162
|
}
|
|
177
163
|
async searchReadableCharges(opts) {
|
|
178
|
-
const employer_ids =
|
|
164
|
+
const employer_ids = employmentEmployerIDs(opts.context, opts.employer_scope_ids);
|
|
179
165
|
return await this.searchReadable({
|
|
180
166
|
...opts,
|
|
181
167
|
// owner_id identifies the creator; employer_id identifies their employer.
|
|
182
168
|
// An explicitly empty authorized employer set must not become All.
|
|
183
|
-
allowed_owner_ids: employer_ids?.
|
|
169
|
+
allowed_owner_ids: employer_ids?.length === 0 ? [] : employmentActivityOwnerIDs(opts.context, true),
|
|
184
170
|
query: mergeQueryOptions(opts.query, chargeSearchQuery(opts), employer_ids === undefined ? undefined : queryParams({
|
|
185
171
|
employer_id: [...employer_ids],
|
|
186
172
|
})),
|
|
@@ -284,11 +270,11 @@ export class BillableFX {
|
|
|
284
270
|
});
|
|
285
271
|
}
|
|
286
272
|
async searchReadableMeterReadings(opts) {
|
|
287
|
-
const employer_ids =
|
|
273
|
+
const employer_ids = employmentEmployerIDs(opts.context, opts.employer_scope_ids);
|
|
288
274
|
return await this.searchReadable({
|
|
289
275
|
...opts,
|
|
290
276
|
// An explicitly empty authorized scope must stay empty, not become All.
|
|
291
|
-
allowed_owner_ids: employer_ids?.
|
|
277
|
+
allowed_owner_ids: employer_ids?.length === 0 ? [] : employmentActivityOwnerIDs(opts.context, true),
|
|
292
278
|
query: mergeQueryOptions(opts.query, meterReadingSearchQuery(opts), employer_ids === undefined ? undefined : queryParams({
|
|
293
279
|
employer_id: [...employer_ids],
|
|
294
280
|
})),
|
|
@@ -743,28 +729,14 @@ export function billableCanDeleteRecord(context, resource, record) {
|
|
|
743
729
|
return billableCanWriteRecord(context, resource, record);
|
|
744
730
|
}
|
|
745
731
|
export function billableOwnsRecord(context, record) {
|
|
746
|
-
return
|
|
732
|
+
return employmentOwnsRecord(context, record);
|
|
747
733
|
}
|
|
748
734
|
export function billableCatalogOwnerIDs(context) {
|
|
749
735
|
return catalogOwnerIDs(context);
|
|
750
736
|
}
|
|
751
737
|
function billableRecordBelongsToActor(context, resource, record) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
if (record.owner_id === context.owner_id) {
|
|
756
|
-
return true;
|
|
757
|
-
}
|
|
758
|
-
return (context.active_mode === UserTypeEmployer &&
|
|
759
|
-
(resource === BillableReadResource.Charge ||
|
|
760
|
-
resource === BillableReadResource.MeterReading) &&
|
|
761
|
-
record.employer_id === context.owner_id);
|
|
762
|
-
}
|
|
763
|
-
function catalogOwnerIDs(context, employer_scope_ids) {
|
|
764
|
-
if (context.active_mode === UserTypeEmployee) {
|
|
765
|
-
return restrictOwnerIDs(context.employment.active_employer_ids, employer_scope_ids);
|
|
766
|
-
}
|
|
767
|
-
return uniqueIDs([context.owner_id]);
|
|
738
|
+
return employmentOwnsRecord(context, record) || ((resource === BillableReadResource.Charge || resource === BillableReadResource.MeterReading) &&
|
|
739
|
+
employmentManagesRecord(context, record));
|
|
768
740
|
}
|
|
769
741
|
function itemSearchQuery(opts) {
|
|
770
742
|
return mergeQueryOptions(queryParams({
|
|
@@ -815,33 +787,6 @@ function meterReadingSearchQuery(opts) {
|
|
|
815
787
|
: { equipment_id: resolvedID(opts.equipment_id) }),
|
|
816
788
|
});
|
|
817
789
|
}
|
|
818
|
-
function activityEmployerScopeIDs(opts) {
|
|
819
|
-
if (opts.context.active_mode === UserTypeEmployer) {
|
|
820
|
-
return new Set([opts.context.owner_id]);
|
|
821
|
-
}
|
|
822
|
-
if (opts.context.active_mode !== UserTypeEmployee ||
|
|
823
|
-
opts.employer_scope_ids === undefined) {
|
|
824
|
-
return undefined;
|
|
825
|
-
}
|
|
826
|
-
return new Set(restrictOwnerIDs(opts.context.employment.active_employer_ids, opts.employer_scope_ids));
|
|
827
|
-
}
|
|
828
|
-
function activityOwnerIDs(context) {
|
|
829
|
-
if (context.active_mode === UserTypeEmployer) {
|
|
830
|
-
return uniqueIDs([context.owner_id, ...context.employment.related_employee_ids]);
|
|
831
|
-
}
|
|
832
|
-
return uniqueIDs([context.owner_id]);
|
|
833
|
-
}
|
|
834
|
-
function restrictOwnerIDs(allowed, requested) {
|
|
835
|
-
const allowed_ids = uniqueIDs(allowed);
|
|
836
|
-
if (requested === undefined) {
|
|
837
|
-
return allowed_ids;
|
|
838
|
-
}
|
|
839
|
-
const allowed_set = new Set(allowed_ids);
|
|
840
|
-
return uniqueIDs(requested).filter((id) => allowed_set.has(id));
|
|
841
|
-
}
|
|
842
|
-
function uniqueIDs(ids) {
|
|
843
|
-
return [...new Set([...ids].map((id) => id.trim()).filter((id) => id !== ""))];
|
|
844
|
-
}
|
|
845
790
|
export function newBillableFX(opts) {
|
|
846
791
|
return new BillableFX(opts);
|
|
847
792
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { queryParam, } from "@fabriktor/client";
|
|
7
7
|
import { Friend, Hired, IDKey, Na, OldHire, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
|
|
8
8
|
import { errRequired, errResolveFailed, errValidation } from "../core/err.js";
|
|
9
|
-
import { requiredOperationID } from "../core/id.js";
|
|
9
|
+
import { normalizeIDs, requiredOperationID } from "../core/id.js";
|
|
10
|
+
import { mergeRecordsByID } from "../core/scope.js";
|
|
10
11
|
import { runSearch } from "../core/search.js";
|
|
11
12
|
const presenceIntervalMS = 120_000;
|
|
12
13
|
const employmentUserBatchSize = 30;
|
|
@@ -245,7 +246,7 @@ export class ContactsFX {
|
|
|
245
246
|
});
|
|
246
247
|
}
|
|
247
248
|
async usersByIDs(uid, ids) {
|
|
248
|
-
const values =
|
|
249
|
+
const values = normalizeIDs(ids);
|
|
249
250
|
if (values.length === 0) {
|
|
250
251
|
return [];
|
|
251
252
|
}
|
|
@@ -257,13 +258,7 @@ export class ContactsFX {
|
|
|
257
258
|
uid,
|
|
258
259
|
}),
|
|
259
260
|
})));
|
|
260
|
-
|
|
261
|
-
for (const page of pages) {
|
|
262
|
-
for (const user of page.results) {
|
|
263
|
-
users.set(user.id, user);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return [...users.values()];
|
|
261
|
+
return mergeRecordsByID(pages.map((page) => page.results));
|
|
267
262
|
}
|
|
268
263
|
async findContact(id) {
|
|
269
264
|
const out = await this.contacts.findOneContact({
|
|
@@ -290,9 +285,6 @@ export function emptyEmploymentOwnerScope() {
|
|
|
290
285
|
related_employee_ids: [],
|
|
291
286
|
};
|
|
292
287
|
}
|
|
293
|
-
function uniqueIDs(ids) {
|
|
294
|
-
return [...new Set([...ids].map((id) => id.trim()).filter((id) => id !== ""))];
|
|
295
|
-
}
|
|
296
288
|
function chunks(values, size) {
|
|
297
289
|
const out = [];
|
|
298
290
|
for (let i = 0; i < values.length; i += size) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Owner, type User, type WeekSheet } from "@fabriktor/schema";
|
|
2
|
+
import type { ContactsFXOperator, EmploymentOwnerScope, EmploymentOwnerScopeOptions } from "./contacts.js";
|
|
3
|
+
export type EmploymentActor = Owner & Pick<User, "active_mode">;
|
|
4
|
+
export type EmploymentReadContext = EmploymentActor & {
|
|
5
|
+
employment: EmploymentOwnerScope;
|
|
6
|
+
};
|
|
7
|
+
export declare function loadEmploymentReadContext(contacts: ContactsFXOperator, opts: EmploymentOwnerScopeOptions): Promise<EmploymentReadContext>;
|
|
8
|
+
export declare function employmentCanRead(context: EmploymentActor): boolean;
|
|
9
|
+
export declare function employmentOwnsRecord(context: EmploymentActor, record: Owner | null | undefined): boolean;
|
|
10
|
+
export declare function employmentManagesRecord(context: EmploymentActor, record: Partial<Pick<WeekSheet, "employer_id">> | null | undefined): boolean;
|
|
11
|
+
export declare function employmentEmployeeIDs(context: EmploymentReadContext): string[];
|
|
12
|
+
export declare function employmentActivityOwnerIDs(context: EmploymentReadContext, include_employer?: boolean): string[];
|
|
13
|
+
export declare function employmentEmployerIDs(context: EmploymentReadContext, requested?: Iterable<string>): string[] | undefined;
|
|
14
|
+
export declare function employmentCatalogOwnerIDs(context: EmploymentReadContext, requested?: Iterable<string>): string[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
|
|
2
|
+
import { resolvedID } from "../core/id.js";
|
|
3
|
+
import { restrictOwnerIDs } from "../core/scope.js";
|
|
4
|
+
export async function loadEmploymentReadContext(contacts, opts) {
|
|
5
|
+
const owner_id = opts.owner_id.trim();
|
|
6
|
+
const employment = await contacts.employmentOwnerScope({ ...opts, owner_id });
|
|
7
|
+
return { owner_id, active_mode: opts.active_mode, employment };
|
|
8
|
+
}
|
|
9
|
+
export function employmentCanRead(context) {
|
|
10
|
+
return resolvedID(context.owner_id) !== undefined && (context.active_mode === UserTypeEmployee || context.active_mode === UserTypeEmployer);
|
|
11
|
+
}
|
|
12
|
+
export function employmentOwnsRecord(context, record) {
|
|
13
|
+
return record != null && record.owner_id === context.owner_id;
|
|
14
|
+
}
|
|
15
|
+
export function employmentManagesRecord(context, record) {
|
|
16
|
+
return record != null && context.active_mode === UserTypeEmployer &&
|
|
17
|
+
record.employer_id === context.owner_id;
|
|
18
|
+
}
|
|
19
|
+
export function employmentEmployeeIDs(context) {
|
|
20
|
+
return employmentCanRead(context) && context.active_mode === UserTypeEmployer
|
|
21
|
+
? restrictOwnerIDs(context.employment.related_employee_ids)
|
|
22
|
+
: [];
|
|
23
|
+
}
|
|
24
|
+
export function employmentActivityOwnerIDs(context, include_employer = false) {
|
|
25
|
+
if (!employmentCanRead(context))
|
|
26
|
+
return [];
|
|
27
|
+
return context.active_mode === UserTypeEmployer
|
|
28
|
+
? restrictOwnerIDs([
|
|
29
|
+
...(include_employer ? [context.owner_id] : []),
|
|
30
|
+
...employmentEmployeeIDs(context),
|
|
31
|
+
])
|
|
32
|
+
: restrictOwnerIDs([context.owner_id]);
|
|
33
|
+
}
|
|
34
|
+
export function employmentEmployerIDs(context, requested) {
|
|
35
|
+
if (!employmentCanRead(context))
|
|
36
|
+
return [];
|
|
37
|
+
if (context.active_mode === UserTypeEmployer)
|
|
38
|
+
return restrictOwnerIDs([context.owner_id], requested);
|
|
39
|
+
return requested === undefined
|
|
40
|
+
? undefined
|
|
41
|
+
: restrictOwnerIDs(context.employment.active_employer_ids, requested);
|
|
42
|
+
}
|
|
43
|
+
export function employmentCatalogOwnerIDs(context, requested) {
|
|
44
|
+
if (!employmentCanRead(context))
|
|
45
|
+
return [];
|
|
46
|
+
return context.active_mode === UserTypeEmployee
|
|
47
|
+
? restrictOwnerIDs(context.employment.active_employer_ids, requested)
|
|
48
|
+
: restrictOwnerIDs([context.owner_id]);
|
|
49
|
+
}
|
package/dist/src/core/id.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { type OperationResult } from "@fabriktor/schema";
|
|
2
2
|
export declare function requiredOperationID(out: OperationResult, msg: string): string;
|
|
3
3
|
export declare function resolvedID(value?: string): string | undefined;
|
|
4
|
+
export declare function normalizeIDs(ids: Iterable<string>): string[];
|
package/dist/src/core/id.js
CHANGED
|
@@ -22,3 +22,12 @@ export function resolvedID(value) {
|
|
|
22
22
|
}
|
|
23
23
|
return id;
|
|
24
24
|
}
|
|
25
|
+
export function normalizeIDs(ids) {
|
|
26
|
+
const values = new Set();
|
|
27
|
+
for (const raw of ids) {
|
|
28
|
+
const id = resolvedID(raw);
|
|
29
|
+
if (id !== undefined)
|
|
30
|
+
values.add(id);
|
|
31
|
+
}
|
|
32
|
+
return [...values];
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FieldOf, OperationResultOf, QueryOptions, QueryOptionsCRUD } from "@fabriktor/client";
|
|
2
|
+
import { type IdentifiedRecord } from "./scope.js";
|
|
3
|
+
export type OwnerScopeQueryOptions<T> = QueryOptionsCRUD & {
|
|
4
|
+
owner_ids: Iterable<string>;
|
|
5
|
+
fetch: (opts: QueryOptionsCRUD) => Promise<OperationResultOf<T[]>>;
|
|
6
|
+
};
|
|
7
|
+
export declare function withOwnerQuery(owner_id: string, query?: QueryOptions): QueryOptions;
|
|
8
|
+
export declare function queryRecordParams<T>(field: FieldOf<T>, query: QueryOptions): QueryOptions;
|
|
9
|
+
export declare function runOwnerScopeQuery<T extends IdentifiedRecord>(opts: OwnerScopeQueryOptions<T>): Promise<T[]>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OwnerIDKey } from "@fabriktor/schema";
|
|
2
|
+
import { mapOwnerScope, mergeRecordsByID } from "./scope.js";
|
|
3
|
+
export function withOwnerQuery(owner_id, query) {
|
|
4
|
+
return { ...query, [OwnerIDKey]: owner_id };
|
|
5
|
+
}
|
|
6
|
+
export function queryRecordParams(field, query) {
|
|
7
|
+
return Object.fromEntries(Object.entries(query).map(([key, value]) => [`${field}.${key}`, value]));
|
|
8
|
+
}
|
|
9
|
+
export async function runOwnerScopeQuery(opts) {
|
|
10
|
+
const responses = await mapOwnerScope(opts.owner_ids, (owner_id) => opts.fetch({
|
|
11
|
+
query: withOwnerQuery(owner_id, opts.query),
|
|
12
|
+
signal: opts.signal,
|
|
13
|
+
}), opts.signal);
|
|
14
|
+
return mergeRecordsByID(responses.map((response) => response.value ?? []));
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IDKey, type Common } from "@fabriktor/schema";
|
|
2
|
+
export type IdentifiedRecord = Pick<Common, typeof IDKey>;
|
|
3
|
+
export declare function restrictOwnerIDs(allowed: Iterable<string>, requested?: Iterable<string>): string[];
|
|
4
|
+
export declare function mapOwnerScope<T>(owner_ids: Iterable<string>, fetch: (owner_id: string) => Promise<T>, signal?: AbortSignal): Promise<T[]>;
|
|
5
|
+
export declare function mergeRecordsByID<T extends IdentifiedRecord>(groups: Iterable<Iterable<T>>): T[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { IDKey } from "@fabriktor/schema";
|
|
2
|
+
import { normalizeIDs } from "./id.js";
|
|
3
|
+
export function restrictOwnerIDs(allowed, requested) {
|
|
4
|
+
const ids = normalizeIDs(allowed);
|
|
5
|
+
if (requested === undefined)
|
|
6
|
+
return ids;
|
|
7
|
+
const allowed_ids = new Set(ids);
|
|
8
|
+
return normalizeIDs(requested).filter((id) => allowed_ids.has(id));
|
|
9
|
+
}
|
|
10
|
+
export async function mapOwnerScope(owner_ids, fetch, signal) {
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
const results = await Promise.all(normalizeIDs(owner_ids).map(fetch));
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
return results;
|
|
15
|
+
}
|
|
16
|
+
export function mergeRecordsByID(groups) {
|
|
17
|
+
const values = new Map();
|
|
18
|
+
for (const group of groups) {
|
|
19
|
+
for (const value of group)
|
|
20
|
+
values.set(value[IDKey], value);
|
|
21
|
+
}
|
|
22
|
+
return [...values.values()];
|
|
23
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { OperationResultOf, SearchManyOptionsCRUD, SearchResultOf } from "@fabriktor/client";
|
|
2
|
+
import { type IdentifiedRecord } from "./scope.js";
|
|
3
|
+
export { withOwnerQuery } from "./query.js";
|
|
2
4
|
export type SearchQuery = NonNullable<SearchManyOptionsCRUD["query"]>;
|
|
3
5
|
export type SearchPaginationOptions = {
|
|
4
6
|
page?: number;
|
|
@@ -42,14 +44,9 @@ export type SearchPage<T> = {
|
|
|
42
44
|
done: boolean;
|
|
43
45
|
};
|
|
44
46
|
export declare function collectSearchResults<T>(search: (page: number) => Promise<SearchPage<T>>): Promise<T[]>;
|
|
45
|
-
export declare function withOwnerQuery(owner_id: string, query?: SearchQuery): SearchQuery;
|
|
46
47
|
export declare function runOwnerSearch<T>(opts: RunOwnerSearchOptions<T>): Promise<SearchPage<T>>;
|
|
47
|
-
export declare function runOwnerScopeSearch<T extends
|
|
48
|
-
|
|
49
|
-
}>(opts: OwnerScopeSearchOptions<T>): Promise<SearchPage<T>>;
|
|
50
|
-
export declare function runOwnerScopeFilteredSearch<T extends {
|
|
51
|
-
id: string;
|
|
52
|
-
}>(opts: OwnerScopeFilteredSearchOptions<T>): Promise<SearchPage<T>>;
|
|
48
|
+
export declare function runOwnerScopeSearch<T extends IdentifiedRecord>(opts: OwnerScopeSearchOptions<T>): Promise<SearchPage<T>>;
|
|
49
|
+
export declare function runOwnerScopeFilteredSearch<T extends IdentifiedRecord>(opts: OwnerScopeFilteredSearchOptions<T>): Promise<SearchPage<T>>;
|
|
53
50
|
export declare function runSearch<T>(opts: RunSearchOptions<T>): Promise<SearchPage<T>>;
|
|
54
51
|
export declare function normalizeSearchPage(value?: number): number;
|
|
55
52
|
export declare function normalizeSearchPerPage(value?: number): number;
|
package/dist/src/core/search.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
4
|
// not use this file except in compliance with the License. You may obtain
|
|
5
5
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
import {
|
|
6
|
+
import { SearchPageKey, SearchPerPageKey, SearchQueryKey, SearchResultResultsKey, } from "@fabriktor/schema";
|
|
7
|
+
import { normalizeIDs } from "./id.js";
|
|
8
|
+
import { withOwnerQuery } from "./query.js";
|
|
9
|
+
import { mapOwnerScope, mergeRecordsByID } from "./scope.js";
|
|
10
|
+
export { withOwnerQuery } from "./query.js";
|
|
7
11
|
const firstSearchPage = 1;
|
|
8
12
|
const defaultSearchPerPage = 30;
|
|
9
13
|
export async function collectSearchResults(search) {
|
|
@@ -21,12 +25,6 @@ export async function collectSearchResults(search) {
|
|
|
21
25
|
page = out.next_page;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
|
-
export function withOwnerQuery(owner_id, query) {
|
|
25
|
-
return {
|
|
26
|
-
...query,
|
|
27
|
-
[OwnerIDKey]: owner_id,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
28
|
export async function runOwnerSearch(opts) {
|
|
31
29
|
const { owner_id, query, ...searchOpts } = opts;
|
|
32
30
|
return await runSearch({
|
|
@@ -35,13 +33,13 @@ export async function runOwnerSearch(opts) {
|
|
|
35
33
|
});
|
|
36
34
|
}
|
|
37
35
|
export async function runOwnerScopeSearch(opts) {
|
|
38
|
-
const owner_ids =
|
|
36
|
+
const owner_ids = normalizeIDs(opts.owner_ids);
|
|
39
37
|
const page = normalizeSearchPage(opts.page);
|
|
40
38
|
const per_page = normalizeSearchPerPage(opts.per_page);
|
|
41
39
|
if (owner_ids.length === 0) {
|
|
42
40
|
return emptySearchPage(page, per_page);
|
|
43
41
|
}
|
|
44
|
-
const pages = await
|
|
42
|
+
const pages = await mapOwnerScope(owner_ids, (owner_id) => runOwnerSearch({
|
|
45
43
|
owner_id,
|
|
46
44
|
q: opts.q,
|
|
47
45
|
page,
|
|
@@ -49,18 +47,13 @@ export async function runOwnerScopeSearch(opts) {
|
|
|
49
47
|
query: opts.query,
|
|
50
48
|
search: opts.search,
|
|
51
49
|
signal: opts.signal,
|
|
52
|
-
}))
|
|
53
|
-
const results =
|
|
54
|
-
for (const ownerPage of pages) {
|
|
55
|
-
for (const result of ownerPage.results) {
|
|
56
|
-
results.set(result.id, result);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
50
|
+
}), opts.signal);
|
|
51
|
+
const results = mergeRecordsByID(pages.map((ownerPage) => ownerPage.results));
|
|
59
52
|
const has_more = pages.some((ownerPage) => ownerPage.has_more);
|
|
60
53
|
const total_count = sumDefined(pages.map((ownerPage) => ownerPage.total_count));
|
|
61
54
|
const last_page = maxDefined(pages.map((ownerPage) => ownerPage.last_page));
|
|
62
55
|
return {
|
|
63
|
-
results
|
|
56
|
+
results,
|
|
64
57
|
page,
|
|
65
58
|
per_page,
|
|
66
59
|
total_count,
|
|
@@ -75,7 +68,7 @@ export async function runOwnerScopeSearch(opts) {
|
|
|
75
68
|
// intentionally heavier than runOwnerScopeSearch and should only be used when
|
|
76
69
|
// the backend model cannot express the requested relationship as a query rule.
|
|
77
70
|
export async function runOwnerScopeFilteredSearch(opts) {
|
|
78
|
-
const owner_ids =
|
|
71
|
+
const owner_ids = normalizeIDs(opts.owner_ids);
|
|
79
72
|
const page = normalizeSearchPage(opts.page);
|
|
80
73
|
const per_page = normalizeSearchPerPage(opts.per_page);
|
|
81
74
|
if (owner_ids.length === 0) {
|
|
@@ -86,7 +79,7 @@ export async function runOwnerScopeFilteredSearch(opts) {
|
|
|
86
79
|
let source_page = 1;
|
|
87
80
|
while (done.size < owner_ids.length) {
|
|
88
81
|
const active_owner_ids = owner_ids.filter((owner_id) => !done.has(owner_id));
|
|
89
|
-
const pages = await
|
|
82
|
+
const pages = await mapOwnerScope(active_owner_ids, async (owner_id) => ({
|
|
90
83
|
owner_id,
|
|
91
84
|
page: await runOwnerSearch({
|
|
92
85
|
owner_id,
|
|
@@ -97,7 +90,7 @@ export async function runOwnerScopeFilteredSearch(opts) {
|
|
|
97
90
|
search: opts.search,
|
|
98
91
|
signal: opts.signal,
|
|
99
92
|
}),
|
|
100
|
-
}))
|
|
93
|
+
}), opts.signal);
|
|
101
94
|
for (const owner_page of pages) {
|
|
102
95
|
if (owner_page.page.done) {
|
|
103
96
|
done.add(owner_page.owner_id);
|
|
@@ -208,9 +201,6 @@ function hasMoreSearchResults(opts) {
|
|
|
208
201
|
}
|
|
209
202
|
return opts.result_count >= opts.per_page;
|
|
210
203
|
}
|
|
211
|
-
function uniqueOwnerIDs(ids) {
|
|
212
|
-
return [...new Set([...ids].map((id) => id.trim()).filter((id) => id !== ""))];
|
|
213
|
-
}
|
|
214
204
|
function emptySearchPage(page, per_page) {
|
|
215
205
|
return {
|
|
216
206
|
results: [],
|
package/dist/src/fx.js
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -5,14 +5,17 @@ export * from "./chat/chat.js";
|
|
|
5
5
|
export * from "./chat/optimistic.js";
|
|
6
6
|
export * from "./chat/send-queue.js";
|
|
7
7
|
export * from "./contacts/contacts.js";
|
|
8
|
+
export * from "./contacts/scope.js";
|
|
8
9
|
export * from "./core/date.js";
|
|
9
10
|
export * from "./core/err.js";
|
|
10
11
|
export * from "./core/id.js";
|
|
11
12
|
export * from "./core/multi-create.js";
|
|
12
13
|
export * from "./core/pre-action.js";
|
|
13
14
|
export * from "./core/pull.js";
|
|
15
|
+
export * from "./core/query.js";
|
|
14
16
|
export * from "./core/queue-store.js";
|
|
15
17
|
export * from "./core/queue.js";
|
|
18
|
+
export * from "./core/scope.js";
|
|
16
19
|
export * from "./core/search-controller.js";
|
|
17
20
|
export * from "./core/search.js";
|
|
18
21
|
export * from "./feed/feed.js";
|
|
@@ -30,7 +33,9 @@ export * from "./processes/processes.js";
|
|
|
30
33
|
export * from "./routes/routes.js";
|
|
31
34
|
export * from "./session/session.js";
|
|
32
35
|
export * from "./storage/storage.js";
|
|
36
|
+
export * from "./timesheets/access.js";
|
|
33
37
|
export * from "./timesheets/timesheets.js";
|
|
38
|
+
export * from "./timesheets/workspace.js";
|
|
34
39
|
export * from "./users/email.js";
|
|
35
40
|
export * from "./users/phone.js";
|
|
36
41
|
export * from "./users/username.js";
|
package/dist/src/index.js
CHANGED
|
@@ -5,14 +5,17 @@ export * from "./chat/chat.js";
|
|
|
5
5
|
export * from "./chat/optimistic.js";
|
|
6
6
|
export * from "./chat/send-queue.js";
|
|
7
7
|
export * from "./contacts/contacts.js";
|
|
8
|
+
export * from "./contacts/scope.js";
|
|
8
9
|
export * from "./core/date.js";
|
|
9
10
|
export * from "./core/err.js";
|
|
10
11
|
export * from "./core/id.js";
|
|
11
12
|
export * from "./core/multi-create.js";
|
|
12
13
|
export * from "./core/pre-action.js";
|
|
13
14
|
export * from "./core/pull.js";
|
|
15
|
+
export * from "./core/query.js";
|
|
14
16
|
export * from "./core/queue-store.js";
|
|
15
17
|
export * from "./core/queue.js";
|
|
18
|
+
export * from "./core/scope.js";
|
|
16
19
|
export * from "./core/search-controller.js";
|
|
17
20
|
export * from "./core/search.js";
|
|
18
21
|
export * from "./feed/feed.js";
|
|
@@ -30,7 +33,9 @@ export * from "./processes/processes.js";
|
|
|
30
33
|
export * from "./routes/routes.js";
|
|
31
34
|
export * from "./session/session.js";
|
|
32
35
|
export * from "./storage/storage.js";
|
|
36
|
+
export * from "./timesheets/access.js";
|
|
33
37
|
export * from "./timesheets/timesheets.js";
|
|
38
|
+
export * from "./timesheets/workspace.js";
|
|
34
39
|
export * from "./users/email.js";
|
|
35
40
|
export * from "./users/phone.js";
|
|
36
41
|
export * from "./users/username.js";
|
package/dist/src/jobs/jobs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Cancelled, Completed, Pending, RecurrenceEndTypeAfterDate, RecurrenceEn
|
|
|
7
7
|
import { queryParams } from "@fabriktor/client";
|
|
8
8
|
import { isZeroDate, normalizeInputDate, zeroInputDate, } from "../core/date.js";
|
|
9
9
|
import { errRequired, errResolveFailed, errValidation, } from "../core/err.js";
|
|
10
|
-
import { requiredOperationID, resolvedID } from "../core/id.js";
|
|
10
|
+
import { normalizeIDs, requiredOperationID, resolvedID } from "../core/id.js";
|
|
11
11
|
import { runPreActionCheck } from "../core/pre-action.js";
|
|
12
12
|
import { runOwnerSearch, runSearch, withOwnerQuery, } from "../core/search.js";
|
|
13
13
|
const msgJobIDRequired = "Job ID is required.";
|
|
@@ -664,19 +664,6 @@ function requiredID(v, field, msg) {
|
|
|
664
664
|
}
|
|
665
665
|
return out;
|
|
666
666
|
}
|
|
667
|
-
function normalizeIDs(ids) {
|
|
668
|
-
const seen = new Set();
|
|
669
|
-
const out = [];
|
|
670
|
-
for (const raw of ids) {
|
|
671
|
-
const id = resolvedID(raw);
|
|
672
|
-
if (id === undefined || seen.has(id)) {
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
seen.add(id);
|
|
676
|
-
out.push(id);
|
|
677
|
-
}
|
|
678
|
-
return out;
|
|
679
|
-
}
|
|
680
667
|
function removedIDs(before, after) {
|
|
681
668
|
const current = new Set(normalizeIDs(after));
|
|
682
669
|
return normalizeIDs(before).filter((id) => !current.has(id));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Summary, type WeekSheet } from "@fabriktor/schema";
|
|
2
|
+
import type { EmploymentOwnerScopeOptions } from "../contacts/contacts.js";
|
|
3
|
+
import { type EmploymentActor, type EmploymentReadContext } from "../contacts/scope.js";
|
|
4
|
+
import { type TimesheetFilters } from "./workspace.js";
|
|
5
|
+
export type LoadTimesheetsReadContextOptions = EmploymentOwnerScopeOptions;
|
|
6
|
+
export type TimesheetsReadContext = EmploymentReadContext;
|
|
7
|
+
export type TimesheetsActor = EmploymentActor;
|
|
8
|
+
export type TimesheetsReadOptions = TimesheetFilters & {
|
|
9
|
+
context: TimesheetsReadContext;
|
|
10
|
+
};
|
|
11
|
+
export type QueryTimesheetSyncDestinationsOptions = {
|
|
12
|
+
context: TimesheetsReadContext;
|
|
13
|
+
source: WeekSheet;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
};
|
|
16
|
+
type TimesheetRecord = WeekSheet | Summary;
|
|
17
|
+
export declare function timesheetCanReadResource(context: TimesheetsActor): boolean;
|
|
18
|
+
export declare function timesheetCanCreateWeek(context: TimesheetsActor): boolean;
|
|
19
|
+
export declare function timesheetCanReadRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
|
|
20
|
+
export declare function timesheetCanWriteRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
|
|
21
|
+
export declare function timesheetCanDeleteRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
|
|
22
|
+
export declare function timesheetCanApproveRecord(context: TimesheetsActor, record: WeekSheet | null | undefined): boolean;
|
|
23
|
+
export declare function timesheetCanEditDay(context: TimesheetsActor, record: WeekSheet | null | undefined, week_day: number): boolean;
|
|
24
|
+
export declare function timesheetCanSyncDestination(context: TimesheetsActor, source: WeekSheet, destination: WeekSheet): boolean;
|
|
25
|
+
export declare function timesheetEmployeeScopeIDs(context: TimesheetsReadContext): string[];
|
|
26
|
+
export declare function timesheetReadFilters(opts: TimesheetsReadOptions): TimesheetFilters;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PunchStatusWorking, UserTypeEmployee, } from "@fabriktor/schema";
|
|
2
|
+
import { employmentActivityOwnerIDs, employmentCanRead, employmentEmployeeIDs, employmentEmployerIDs, employmentManagesRecord, employmentOwnsRecord, } from "../contacts/scope.js";
|
|
3
|
+
import { restrictOwnerIDs } from "../core/scope.js";
|
|
4
|
+
import { timesheetCanApprove, timesheetCanSync, timesheetHasWorkingPunches, timesheetIsMutable, } from "./workspace.js";
|
|
5
|
+
export function timesheetCanReadResource(context) {
|
|
6
|
+
return employmentCanRead(context);
|
|
7
|
+
}
|
|
8
|
+
export function timesheetCanCreateWeek(context) {
|
|
9
|
+
return timesheetCanReadResource(context) && context.active_mode === UserTypeEmployee;
|
|
10
|
+
}
|
|
11
|
+
export function timesheetCanReadRecord(context, record) {
|
|
12
|
+
return timesheetCanReadResource(context) && record != null && (employmentOwnsRecord(context, record) || employmentManagesRecord(context, record));
|
|
13
|
+
}
|
|
14
|
+
export function timesheetCanWriteRecord(context, record) {
|
|
15
|
+
return record != null && timesheetCanReadRecord(context, record) && (!("sheets" in record) || timesheetIsMutable(record));
|
|
16
|
+
}
|
|
17
|
+
export function timesheetCanDeleteRecord(context, record) {
|
|
18
|
+
return record != null && timesheetCanWriteRecord(context, record) && (!("sheets" in record) || !timesheetHasWorkingPunches(record));
|
|
19
|
+
}
|
|
20
|
+
export function timesheetCanApproveRecord(context, record) {
|
|
21
|
+
return record != null && timesheetCanReadResource(context) &&
|
|
22
|
+
employmentManagesRecord(context, record) && timesheetCanApprove(record);
|
|
23
|
+
}
|
|
24
|
+
export function timesheetCanEditDay(context, record, week_day) {
|
|
25
|
+
if (record == null || !timesheetCanCreateWeek(context) ||
|
|
26
|
+
!employmentOwnsRecord(context, record) || !timesheetIsMutable(record))
|
|
27
|
+
return false;
|
|
28
|
+
const day = record.sheets.find((value) => value.week_day === week_day);
|
|
29
|
+
return day !== undefined && day.punches.length > 0 &&
|
|
30
|
+
day.punches.every((punch) => punch.punch_status !== PunchStatusWorking);
|
|
31
|
+
}
|
|
32
|
+
export function timesheetCanSyncDestination(context, source, destination) {
|
|
33
|
+
return timesheetCanApproveRecord(context, source) &&
|
|
34
|
+
timesheetCanWriteRecord(context, destination) && timesheetCanSync(source, destination);
|
|
35
|
+
}
|
|
36
|
+
export function timesheetEmployeeScopeIDs(context) {
|
|
37
|
+
return employmentEmployeeIDs(context);
|
|
38
|
+
}
|
|
39
|
+
export function timesheetReadFilters(opts) {
|
|
40
|
+
const { context, ...filters } = opts;
|
|
41
|
+
if (!timesheetCanReadResource(context))
|
|
42
|
+
return { ...filters, owner_ids: [] };
|
|
43
|
+
return {
|
|
44
|
+
...filters,
|
|
45
|
+
owner_ids: restrictOwnerIDs(employmentActivityOwnerIDs(context), opts.owner_ids),
|
|
46
|
+
employer_ids: employmentEmployerIDs(context, opts.employer_ids),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type Date as SchemaDate, type OperationResult, type Summary, type WeekSheet } from "@fabriktor/schema";
|
|
2
|
-
import type { TimesheetsOperator } from "@fabriktor/client";
|
|
2
|
+
import type { BillableOperator, TimesheetsOperator } from "@fabriktor/client";
|
|
3
|
+
import type { ContactsFXOperator } from "../contacts/contacts.js";
|
|
4
|
+
import { type LoadTimesheetsReadContextOptions, type QueryTimesheetSyncDestinationsOptions, type TimesheetsReadContext, type TimesheetsReadOptions } from "./access.js";
|
|
5
|
+
import { TimesheetsWorkspace } from "./workspace.js";
|
|
3
6
|
type PunchPayload = Parameters<TimesheetsOperator["punch"]>[0];
|
|
4
7
|
type WeekSheetReplaceOptions = Parameters<TimesheetsOperator["replaceOneWeekSheet"]>[0];
|
|
5
8
|
type SummaryReplaceOptions = Parameters<TimesheetsOperator["replaceOneSummary"]>[0];
|
|
@@ -7,6 +10,9 @@ export type ReplaceWeekSheetOptions = WeekSheetReplaceOptions;
|
|
|
7
10
|
export type ReplaceSummaryOptions = SummaryReplaceOptions;
|
|
8
11
|
export type TimesheetsFXOptions = {
|
|
9
12
|
timesheets: TimesheetsOperator;
|
|
13
|
+
contacts_fx: ContactsFXOperator;
|
|
14
|
+
billable?: BillableOperator;
|
|
15
|
+
now?: () => globalThis.Date;
|
|
10
16
|
};
|
|
11
17
|
export type PunchContext = Pick<PunchPayload, "timezone" | "location_id" | "employer_id">;
|
|
12
18
|
export type PunchInOptions = PunchContext & {
|
|
@@ -61,6 +67,11 @@ export type ApproveWeekSheetsOptions = {
|
|
|
61
67
|
week_sheets: WeekSheet[];
|
|
62
68
|
};
|
|
63
69
|
export interface TimesheetsFXOperator {
|
|
70
|
+
readonly workspace: TimesheetsWorkspace;
|
|
71
|
+
loadReadContext(opts: LoadTimesheetsReadContextOptions): Promise<TimesheetsReadContext>;
|
|
72
|
+
queryReadableWeekSheets(opts: TimesheetsReadOptions): Promise<WeekSheet[]>;
|
|
73
|
+
queryReadableSummaries(opts: TimesheetsReadOptions): Promise<Summary[]>;
|
|
74
|
+
querySyncDestinations(opts: QueryTimesheetSyncDestinationsOptions): Promise<WeekSheet[]>;
|
|
64
75
|
replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
|
|
65
76
|
replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
|
|
66
77
|
punchControls(opts: PunchControlsOptions): PunchControls;
|
|
@@ -76,9 +87,15 @@ export interface TimesheetsFXOperator {
|
|
|
76
87
|
approveWeekSheets(opts: ApproveWeekSheetsOptions): Promise<void>;
|
|
77
88
|
}
|
|
78
89
|
export declare class TimesheetsFX implements TimesheetsFXOperator {
|
|
90
|
+
readonly workspace: TimesheetsWorkspace;
|
|
79
91
|
private timesheets;
|
|
92
|
+
private contacts_fx;
|
|
80
93
|
private punch_in_progress;
|
|
81
94
|
constructor(opts: TimesheetsFXOptions);
|
|
95
|
+
loadReadContext(opts: LoadTimesheetsReadContextOptions): Promise<TimesheetsReadContext>;
|
|
96
|
+
queryReadableWeekSheets(opts: TimesheetsReadOptions): Promise<WeekSheet[]>;
|
|
97
|
+
queryReadableSummaries(opts: TimesheetsReadOptions): Promise<Summary[]>;
|
|
98
|
+
querySyncDestinations(opts: QueryTimesheetSyncDestinationsOptions): Promise<WeekSheet[]>;
|
|
82
99
|
replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
|
|
83
100
|
replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
|
|
84
101
|
punchControls(opts: PunchControlsOptions): PunchControls;
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
import { PunchModeDefault, PunchModeDelete, PunchModeReplace, PunchModeWeekSheet, PunchStatusPending, PunchStatusWorking, PunchTypeIn, PunchTypeOut, PunchTypeRideIn, PunchTypeRideOut, ZeroID, } from "@fabriktor/schema";
|
|
7
7
|
import { zeroDate } from "../core/date.js";
|
|
8
8
|
import { errRequired, errResolveFailed, errValidation, } from "../core/err.js";
|
|
9
|
+
import { normalizeIDs } from "../core/id.js";
|
|
10
|
+
import { loadEmploymentReadContext } from "../contacts/scope.js";
|
|
11
|
+
import { PreActionBlocked } from "../core/pre-action.js";
|
|
12
|
+
import { timesheetCanApproveRecord, timesheetCanSyncDestination, timesheetReadFilters, } from "./access.js";
|
|
13
|
+
import { TimesheetsWorkspace, TimesheetsBlock, timesheetCanApprove, timesheetCanSync, timesheetIsMutable, } from "./workspace.js";
|
|
9
14
|
const allWeekDays = [0, 1, 2, 3, 4, 5, 6];
|
|
10
15
|
const minuteStep = 15;
|
|
11
16
|
const msgPunchInProgress = "A punch action is already in progress.";
|
|
@@ -27,11 +32,39 @@ const resourceSummary = "summary";
|
|
|
27
32
|
const msgWeekSheetMissing = "Week sheet could not be resolved after the mutation.";
|
|
28
33
|
const msgSummaryMissing = "Summary could not be resolved after the mutation.";
|
|
29
34
|
export class TimesheetsFX {
|
|
35
|
+
workspace;
|
|
30
36
|
timesheets;
|
|
37
|
+
contacts_fx;
|
|
31
38
|
punch_in_progress;
|
|
32
39
|
constructor(opts) {
|
|
33
40
|
this.timesheets = opts.timesheets;
|
|
41
|
+
this.contacts_fx = opts.contacts_fx;
|
|
34
42
|
this.punch_in_progress = false;
|
|
43
|
+
this.workspace = new TimesheetsWorkspace(opts.timesheets, this, opts.billable, opts.now);
|
|
44
|
+
}
|
|
45
|
+
async loadReadContext(opts) {
|
|
46
|
+
return await loadEmploymentReadContext(this.contacts_fx, opts);
|
|
47
|
+
}
|
|
48
|
+
async queryReadableWeekSheets(opts) {
|
|
49
|
+
return await this.workspace.weeks(timesheetReadFilters(opts));
|
|
50
|
+
}
|
|
51
|
+
async queryReadableSummaries(opts) {
|
|
52
|
+
return await this.workspace.summaries(timesheetReadFilters(opts));
|
|
53
|
+
}
|
|
54
|
+
async querySyncDestinations(opts) {
|
|
55
|
+
const source = await this.workspace.week(opts.source.id, opts.signal);
|
|
56
|
+
if (source.updated_at !== opts.source.updated_at)
|
|
57
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
58
|
+
if (!timesheetCanApproveRecord(opts.context, source))
|
|
59
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
60
|
+
const weeks = await this.queryReadableWeekSheets({
|
|
61
|
+
context: opts.context,
|
|
62
|
+
employer_ids: [source.employer_id],
|
|
63
|
+
period_indexes: [source.period_index],
|
|
64
|
+
is_archived: false,
|
|
65
|
+
signal: opts.signal,
|
|
66
|
+
});
|
|
67
|
+
return weeks.filter((week) => timesheetCanSyncDestination(opts.context, source, week));
|
|
35
68
|
}
|
|
36
69
|
async replaceWeekSheet(opts) {
|
|
37
70
|
await this.timesheets.replaceOneWeekSheet(opts);
|
|
@@ -43,7 +76,7 @@ export class TimesheetsFX {
|
|
|
43
76
|
}
|
|
44
77
|
punchControls(opts) {
|
|
45
78
|
validateWeekDay(opts.week_day, "week_day");
|
|
46
|
-
if (opts.week_sheet
|
|
79
|
+
if (opts.week_sheet && !timesheetIsMutable(opts.week_sheet)) {
|
|
47
80
|
return disabledPunchControls();
|
|
48
81
|
}
|
|
49
82
|
const day = opts.week_sheet?.sheets.find((sheet) => sheet.week_day === opts.week_day);
|
|
@@ -207,7 +240,13 @@ export class TimesheetsFX {
|
|
|
207
240
|
});
|
|
208
241
|
}
|
|
209
242
|
const ids = [];
|
|
210
|
-
for (const
|
|
243
|
+
for (const expected of opts.week_sheets) {
|
|
244
|
+
// Query cleaners can return partial days. Always authorize the whole fresh record.
|
|
245
|
+
const week_sheet = await this.findWeekSheet(expected.id);
|
|
246
|
+
if (week_sheet.updated_at !== expected.updated_at)
|
|
247
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
248
|
+
if (!timesheetCanApprove(week_sheet))
|
|
249
|
+
throw new PreActionBlocked(TimesheetsBlock.Locked);
|
|
211
250
|
const id = requiredID(week_sheet.id, "week_sheets", msgWeekSheetIDRequired);
|
|
212
251
|
if (week_sheet.employer_id !== employer_id) {
|
|
213
252
|
throw errValidation({
|
|
@@ -266,6 +305,12 @@ export class TimesheetsFX {
|
|
|
266
305
|
return summary;
|
|
267
306
|
}
|
|
268
307
|
async syncSheets(opts) {
|
|
308
|
+
const source = await this.findWeekSheet(opts.from.id);
|
|
309
|
+
if (source.updated_at !== opts.from.updated_at)
|
|
310
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
311
|
+
if (!timesheetCanApprove(source))
|
|
312
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
313
|
+
opts = { ...opts, from: source };
|
|
269
314
|
const from_id = requiredID(opts.from.id, "from", msgWeekSheetIDRequired);
|
|
270
315
|
if (opts.to.length === 0) {
|
|
271
316
|
throw errRequired({
|
|
@@ -274,20 +319,19 @@ export class TimesheetsFX {
|
|
|
274
319
|
});
|
|
275
320
|
}
|
|
276
321
|
const to_ids = [];
|
|
277
|
-
for (const
|
|
322
|
+
for (const expected of opts.to) {
|
|
323
|
+
const week_sheet = await this.findWeekSheet(expected.id);
|
|
324
|
+
if (week_sheet.updated_at !== expected.updated_at)
|
|
325
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
278
326
|
const id = requiredID(week_sheet.id, "to", msgWeekSheetIDRequired);
|
|
279
|
-
if (week_sheet.is_archived) {
|
|
280
|
-
throw errValidation({
|
|
281
|
-
field: "to",
|
|
282
|
-
msg: msgWeekSheetArchived,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
327
|
if (week_sheet.employer_id !== opts.from.employer_id) {
|
|
286
328
|
throw errValidation({
|
|
287
329
|
field: "to",
|
|
288
330
|
msg: msgSyncEmployerMismatch,
|
|
289
331
|
});
|
|
290
332
|
}
|
|
333
|
+
if (!timesheetCanSync(opts.from, week_sheet))
|
|
334
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
291
335
|
to_ids.push(id);
|
|
292
336
|
}
|
|
293
337
|
const input = {
|
|
@@ -420,16 +464,3 @@ function requiredText(v, field, msg) {
|
|
|
420
464
|
}
|
|
421
465
|
return out;
|
|
422
466
|
}
|
|
423
|
-
function normalizeIDs(ids) {
|
|
424
|
-
const seen = new Set();
|
|
425
|
-
const out = [];
|
|
426
|
-
for (const raw of ids) {
|
|
427
|
-
const id = raw.trim();
|
|
428
|
-
if (id === "" || id === ZeroID || seen.has(id)) {
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
seen.add(id);
|
|
432
|
-
out.push(id);
|
|
433
|
-
}
|
|
434
|
-
return out;
|
|
435
|
-
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type BillableOperator, type TimesheetsOperator } from "@fabriktor/client";
|
|
2
|
+
import { PunchTypeIn, PunchTypeOut, PunchTypeRideIn, PunchTypeRideOut, type InDate, type Punch, type Summary, type WeekSheet } from "@fabriktor/schema";
|
|
3
|
+
import type { TimesheetsFXOperator } from "./timesheets.js";
|
|
4
|
+
export type TimesheetFilters = {
|
|
5
|
+
owner_ids?: WeekSheet["owner_id"][];
|
|
6
|
+
employer_ids?: WeekSheet["employer_id"][];
|
|
7
|
+
period_indexes?: WeekSheet["period_index"][];
|
|
8
|
+
location_ids?: Punch["location_id"][];
|
|
9
|
+
is_archived?: WeekSheet["is_archived"];
|
|
10
|
+
punch_status?: Punch["punch_status"];
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
};
|
|
13
|
+
export type PunchStateOptions = {
|
|
14
|
+
owner_id: string;
|
|
15
|
+
employer_id: string;
|
|
16
|
+
timezone: string;
|
|
17
|
+
date?: InDate;
|
|
18
|
+
};
|
|
19
|
+
export type TimesheetPunchState = {
|
|
20
|
+
week_sheet?: WeekSheet;
|
|
21
|
+
punch?: Punch;
|
|
22
|
+
week_day: number;
|
|
23
|
+
stale_day: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type TimesheetPunchAction = typeof PunchTypeIn | typeof PunchTypeOut | typeof PunchTypeRideIn | typeof PunchTypeRideOut;
|
|
26
|
+
export type CheckedPunchOptions = PunchStateOptions & {
|
|
27
|
+
location_id: string;
|
|
28
|
+
action: TimesheetPunchAction;
|
|
29
|
+
expected: TimesheetPunchState;
|
|
30
|
+
total_pause_minutes?: number;
|
|
31
|
+
total_meal_minutes?: number;
|
|
32
|
+
};
|
|
33
|
+
export type SaveTimesheetDayOptions = PunchStateOptions & {
|
|
34
|
+
location_id: string;
|
|
35
|
+
start: InDate;
|
|
36
|
+
end: InDate;
|
|
37
|
+
total_pause_minutes: number;
|
|
38
|
+
total_meal_minutes: number;
|
|
39
|
+
replace?: WeekSheet;
|
|
40
|
+
};
|
|
41
|
+
export declare const TimesheetsBlock: {
|
|
42
|
+
readonly Changed: "timesheets_changed";
|
|
43
|
+
readonly Locked: "timesheets_locked";
|
|
44
|
+
readonly Active: "timesheets_active";
|
|
45
|
+
readonly InvalidDay: "timesheets_invalid_day";
|
|
46
|
+
readonly InvalidSync: "timesheets_invalid_sync";
|
|
47
|
+
readonly Unconfirmed: "timesheets_unconfirmed";
|
|
48
|
+
};
|
|
49
|
+
export declare function timesheetPunches(sheet: WeekSheet): Punch[];
|
|
50
|
+
export declare function timesheetHasWorkingPunches(sheet: WeekSheet): boolean;
|
|
51
|
+
export declare function timesheetIsMutable(sheet: WeekSheet): boolean;
|
|
52
|
+
export declare function timesheetCanApprove(sheet: WeekSheet): boolean;
|
|
53
|
+
export declare function timesheetCanSync(source: WeekSheet, destination: WeekSheet): boolean;
|
|
54
|
+
export declare function timesheetStateKey(state: TimesheetPunchState): string;
|
|
55
|
+
export declare function timesheetWeekDay(date: InDate): number;
|
|
56
|
+
export declare class TimesheetsWorkspace {
|
|
57
|
+
private readonly client;
|
|
58
|
+
private readonly actions;
|
|
59
|
+
private readonly billable?;
|
|
60
|
+
private readonly now;
|
|
61
|
+
constructor(client: TimesheetsOperator, actions: TimesheetsFXOperator, billable?: BillableOperator | undefined, now?: () => Date);
|
|
62
|
+
weeks(opts: TimesheetFilters): Promise<WeekSheet[]>;
|
|
63
|
+
summaries(opts: TimesheetFilters): Promise<Summary[]>;
|
|
64
|
+
week(id: string, signal?: AbortSignal): Promise<WeekSheet>;
|
|
65
|
+
summary(id: string, signal?: AbortSignal): Promise<Summary>;
|
|
66
|
+
periods(employer_id: string, start: InDate, end?: InDate): Promise<number[]>;
|
|
67
|
+
punchState(opts: PunchStateOptions): Promise<TimesheetPunchState>;
|
|
68
|
+
punch(opts: CheckedPunchOptions): Promise<WeekSheet>;
|
|
69
|
+
saveDay(opts: SaveTimesheetDayOptions): Promise<WeekSheet>;
|
|
70
|
+
removeDay(owner_id: string, expected: WeekSheet, date: InDate): Promise<void>;
|
|
71
|
+
removeWeek(actor_id: string, expected: WeekSheet): Promise<void>;
|
|
72
|
+
blankWeek(opts: PunchStateOptions & {
|
|
73
|
+
location_id: string;
|
|
74
|
+
date: InDate;
|
|
75
|
+
}): Promise<WeekSheet>;
|
|
76
|
+
private queryOwners;
|
|
77
|
+
private checkedWeek;
|
|
78
|
+
private validateDay;
|
|
79
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { mergeQueryOptions, queryNestedParam, queryParams, } from "@fabriktor/client";
|
|
2
|
+
import { PunchStatusApproved, PunchStatusPending, PunchStatusWorking, PunchTypeIn, PunchTypeOut, PunchTypeRideIn, PunchTypeRideOut, } from "@fabriktor/schema";
|
|
3
|
+
import { inDateAtTimestamp, zeroDate } from "../core/date.js";
|
|
4
|
+
import { errRequired } from "../core/err.js";
|
|
5
|
+
import { PreActionBlocked } from "../core/pre-action.js";
|
|
6
|
+
import { queryRecordParams, runOwnerScopeQuery, } from "../core/query.js";
|
|
7
|
+
export const TimesheetsBlock = {
|
|
8
|
+
Changed: "timesheets_changed",
|
|
9
|
+
Locked: "timesheets_locked",
|
|
10
|
+
Active: "timesheets_active",
|
|
11
|
+
InvalidDay: "timesheets_invalid_day",
|
|
12
|
+
InvalidSync: "timesheets_invalid_sync",
|
|
13
|
+
Unconfirmed: "timesheets_unconfirmed",
|
|
14
|
+
};
|
|
15
|
+
function requireState(allowed, reason) {
|
|
16
|
+
if (!allowed)
|
|
17
|
+
throw new PreActionBlocked(reason);
|
|
18
|
+
}
|
|
19
|
+
export function timesheetPunches(sheet) {
|
|
20
|
+
return sheet.sheets.flatMap((day) => day.punches);
|
|
21
|
+
}
|
|
22
|
+
export function timesheetHasWorkingPunches(sheet) {
|
|
23
|
+
return timesheetPunches(sheet).some((punch) => punch.punch_status === PunchStatusWorking);
|
|
24
|
+
}
|
|
25
|
+
export function timesheetIsMutable(sheet) {
|
|
26
|
+
return (!sheet.is_archived &&
|
|
27
|
+
!timesheetPunches(sheet).some((p) => p.punch_status === PunchStatusApproved));
|
|
28
|
+
}
|
|
29
|
+
export function timesheetCanApprove(sheet) {
|
|
30
|
+
const punches = timesheetPunches(sheet);
|
|
31
|
+
return (timesheetIsMutable(sheet) &&
|
|
32
|
+
punches.length > 0 &&
|
|
33
|
+
punches.every((p) => p.punch_status === PunchStatusPending));
|
|
34
|
+
}
|
|
35
|
+
export function timesheetCanSync(source, destination) {
|
|
36
|
+
return timesheetCanApprove(source) && timesheetIsMutable(destination) &&
|
|
37
|
+
!timesheetHasWorkingPunches(destination) &&
|
|
38
|
+
destination.employer_id === source.employer_id &&
|
|
39
|
+
destination.id !== source.id &&
|
|
40
|
+
destination.period_index === source.period_index &&
|
|
41
|
+
destination.timezone === source.timezone;
|
|
42
|
+
}
|
|
43
|
+
export function timesheetStateKey(state) {
|
|
44
|
+
return JSON.stringify([
|
|
45
|
+
state.week_sheet?.id,
|
|
46
|
+
state.week_sheet?.updated_at,
|
|
47
|
+
state.week_sheet?.is_archived,
|
|
48
|
+
state.week_day,
|
|
49
|
+
state.punch,
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
export function timesheetWeekDay(date) {
|
|
53
|
+
// Calendar arithmetic on explicit date parts; never the device timezone.
|
|
54
|
+
return new Date(Date.UTC(date.y, date.m - 1, date.d)).getUTCDay();
|
|
55
|
+
}
|
|
56
|
+
function sameDay(a, b) {
|
|
57
|
+
return a.y === b.y && a.m === b.m && a.d === b.d;
|
|
58
|
+
}
|
|
59
|
+
function wallTime(date) {
|
|
60
|
+
return Date.UTC(date.y, date.m - 1, date.d, date.h, date.min);
|
|
61
|
+
}
|
|
62
|
+
function schemaDate(date) {
|
|
63
|
+
// The backend computes UTC from the supplied wall-clock fields and tz.
|
|
64
|
+
return { ...zeroDate(), ...date };
|
|
65
|
+
}
|
|
66
|
+
export class TimesheetsWorkspace {
|
|
67
|
+
client;
|
|
68
|
+
actions;
|
|
69
|
+
billable;
|
|
70
|
+
now;
|
|
71
|
+
constructor(client, actions, billable, now = () => new Date()) {
|
|
72
|
+
this.client = client;
|
|
73
|
+
this.actions = actions;
|
|
74
|
+
this.billable = billable;
|
|
75
|
+
this.now = now;
|
|
76
|
+
}
|
|
77
|
+
async weeks(opts) {
|
|
78
|
+
return await this.queryOwners(opts, mergeQueryOptions(query(opts), queryParams({ is_archived: opts.is_archived }), queryRecordParams("sheets", mergeQueryOptions(queryNestedParam(["punches", "location_id"], opts.location_ids), queryNestedParam(["punches", "punch_status"], opts.punch_status)))), (queryOpts) => this.client.queryWeekSheets(queryOpts));
|
|
79
|
+
}
|
|
80
|
+
async summaries(opts) {
|
|
81
|
+
return await this.queryOwners(opts, mergeQueryOptions(query(opts), queryNestedParam(["location_records", "location_id"], opts.location_ids)), (queryOpts) => this.client.querySummaries(queryOpts));
|
|
82
|
+
}
|
|
83
|
+
async week(id, signal) {
|
|
84
|
+
const { value } = await this.client.findOneWeekSheet({ id, signal });
|
|
85
|
+
requireState(!!value, TimesheetsBlock.Changed);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
async summary(id, signal) {
|
|
89
|
+
const { value } = await this.client.findOneSummary({ id, signal });
|
|
90
|
+
requireState(!!value, TimesheetsBlock.Changed);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
async periods(employer_id, start, end = start) {
|
|
94
|
+
requireState(!!this.billable && !!employer_id, TimesheetsBlock.InvalidDay);
|
|
95
|
+
const result = await this.billable.periodsFromDates({
|
|
96
|
+
employer_id,
|
|
97
|
+
start,
|
|
98
|
+
end,
|
|
99
|
+
});
|
|
100
|
+
requireState(!!result.value?.ints.length, TimesheetsBlock.InvalidDay);
|
|
101
|
+
return result.value.ints;
|
|
102
|
+
}
|
|
103
|
+
async punchState(opts) {
|
|
104
|
+
requireState(!!opts.owner_id && !!opts.timezone, TimesheetsBlock.InvalidDay);
|
|
105
|
+
const today = opts.date ?? inDateAtTimestamp(this.now().toISOString(), opts.timezone);
|
|
106
|
+
const week_day = timesheetWeekDay(today);
|
|
107
|
+
// Check every open work punch, including other employers/locations/weeks.
|
|
108
|
+
// A scope selection must never hide an already open shift.
|
|
109
|
+
const active = await this.weeks({
|
|
110
|
+
owner_ids: [opts.owner_id],
|
|
111
|
+
is_archived: false,
|
|
112
|
+
punch_status: PunchStatusWorking,
|
|
113
|
+
});
|
|
114
|
+
const open = active.flatMap((week_sheet) => week_sheet.sheets.flatMap((day) => day.punches
|
|
115
|
+
.filter((p) => p.punch_status === PunchStatusWorking)
|
|
116
|
+
.map((punch) => ({ week_sheet, punch, week_day: day.week_day }))));
|
|
117
|
+
requireState(open.length <= 1, TimesheetsBlock.Active);
|
|
118
|
+
if (open[0]) {
|
|
119
|
+
const current = open[0];
|
|
120
|
+
const date = inDateAtTimestamp(this.now().toISOString(), current.punch.timezone);
|
|
121
|
+
return {
|
|
122
|
+
...current,
|
|
123
|
+
stale_day: !sameDay(current.punch.punch_in_time, opts.date ?? date),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (!opts.employer_id)
|
|
127
|
+
return { week_day, stale_day: false };
|
|
128
|
+
const indexes = await this.periods(opts.employer_id, today);
|
|
129
|
+
const weeks = await this.weeks({
|
|
130
|
+
owner_ids: [opts.owner_id],
|
|
131
|
+
employer_ids: [opts.employer_id],
|
|
132
|
+
period_indexes: indexes,
|
|
133
|
+
});
|
|
134
|
+
requireState(weeks.length <= 1, TimesheetsBlock.Changed);
|
|
135
|
+
const week_sheet = weeks[0];
|
|
136
|
+
const punch = week_sheet?.sheets
|
|
137
|
+
.find((day) => day.week_day === week_day)
|
|
138
|
+
?.punches.at(-1);
|
|
139
|
+
return { week_sheet, punch, week_day, stale_day: false };
|
|
140
|
+
}
|
|
141
|
+
async punch(opts) {
|
|
142
|
+
if (opts.date)
|
|
143
|
+
this.validateDay(opts.date, opts.timezone);
|
|
144
|
+
const current = await this.punchState(opts);
|
|
145
|
+
requireState(timesheetStateKey(current) === timesheetStateKey(opts.expected), TimesheetsBlock.Changed);
|
|
146
|
+
requireState(!current.stale_day, TimesheetsBlock.InvalidDay);
|
|
147
|
+
requireState(!current.week_sheet || timesheetIsMutable(current.week_sheet), TimesheetsBlock.Locked);
|
|
148
|
+
const controls = this.actions.punchControls(current);
|
|
149
|
+
const allowed = {
|
|
150
|
+
[PunchTypeIn]: controls.can_punch_in,
|
|
151
|
+
[PunchTypeOut]: controls.can_punch_out,
|
|
152
|
+
[PunchTypeRideIn]: controls.can_ride_in,
|
|
153
|
+
[PunchTypeRideOut]: controls.can_ride_out,
|
|
154
|
+
};
|
|
155
|
+
requireState(allowed[opts.action], TimesheetsBlock.Changed);
|
|
156
|
+
if (opts.action !== PunchTypeIn) {
|
|
157
|
+
requireState(current.punch?.location_id === opts.location_id &&
|
|
158
|
+
current.punch.employer_id === opts.employer_id &&
|
|
159
|
+
current.punch.timezone === opts.timezone, TimesheetsBlock.Changed);
|
|
160
|
+
}
|
|
161
|
+
if (opts.date && current.punch && opts.action !== PunchTypeIn) {
|
|
162
|
+
requireState(sameDay(opts.date, current.punch.punch_in_time) &&
|
|
163
|
+
wallTime(opts.date) >= wallTime(current.punch.punch_in_time), TimesheetsBlock.InvalidDay);
|
|
164
|
+
}
|
|
165
|
+
const context = {
|
|
166
|
+
employer_id: opts.employer_id,
|
|
167
|
+
location_id: opts.location_id,
|
|
168
|
+
timezone: opts.timezone,
|
|
169
|
+
explicit_date: opts.date ? schemaDate(opts.date) : undefined,
|
|
170
|
+
};
|
|
171
|
+
const result = opts.action === PunchTypeIn
|
|
172
|
+
? await this.actions.punchIn(context)
|
|
173
|
+
: opts.action === PunchTypeOut
|
|
174
|
+
? await this.actions.punchOut({
|
|
175
|
+
...context,
|
|
176
|
+
total_pause_minutes: opts.total_pause_minutes ?? 0,
|
|
177
|
+
total_meal_minutes: opts.total_meal_minutes ?? 0,
|
|
178
|
+
})
|
|
179
|
+
: opts.action === PunchTypeRideIn
|
|
180
|
+
? await this.actions.rideIn(context)
|
|
181
|
+
: await this.actions.rideOut(context);
|
|
182
|
+
requireState(!!result.id, TimesheetsBlock.Unconfirmed);
|
|
183
|
+
const sheet = await this.week(result.id);
|
|
184
|
+
const last = sheet.sheets
|
|
185
|
+
.find((day) => day.week_day === current.week_day)
|
|
186
|
+
?.punches.at(-1);
|
|
187
|
+
requireState(!!last &&
|
|
188
|
+
last.location_id === opts.location_id &&
|
|
189
|
+
!sheet.is_archived &&
|
|
190
|
+
(opts.action === PunchTypeOut
|
|
191
|
+
? last.punch_status === PunchStatusPending
|
|
192
|
+
: last.punch_status === PunchStatusWorking) &&
|
|
193
|
+
(opts.action !== PunchTypeRideIn ||
|
|
194
|
+
last.rides.at(-1)?.is_active === true) &&
|
|
195
|
+
(opts.action !== PunchTypeRideOut ||
|
|
196
|
+
last.rides.at(-1)?.is_active === false), TimesheetsBlock.Unconfirmed);
|
|
197
|
+
return sheet;
|
|
198
|
+
}
|
|
199
|
+
async saveDay(opts) {
|
|
200
|
+
this.validateDay(opts.start, opts.timezone);
|
|
201
|
+
this.validateDay(opts.end, opts.timezone);
|
|
202
|
+
requireState(sameDay(opts.start, opts.end) &&
|
|
203
|
+
wallTime(opts.end) > wallTime(opts.start), TimesheetsBlock.InvalidDay);
|
|
204
|
+
const indexes = await this.periods(opts.employer_id, opts.start);
|
|
205
|
+
const matches = await this.weeks({
|
|
206
|
+
owner_ids: [opts.owner_id],
|
|
207
|
+
employer_ids: [opts.employer_id],
|
|
208
|
+
period_indexes: indexes,
|
|
209
|
+
});
|
|
210
|
+
requireState(matches.length <= 1, TimesheetsBlock.Changed);
|
|
211
|
+
const sheet = matches[0];
|
|
212
|
+
const day = sheet?.sheets.find((d) => d.week_day === timesheetWeekDay(opts.start));
|
|
213
|
+
if (opts.replace) {
|
|
214
|
+
requireState(sheet?.id === opts.replace.id &&
|
|
215
|
+
sheet.updated_at === opts.replace.updated_at, TimesheetsBlock.Changed);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// A new manual entry must not silently replace an existing day.
|
|
219
|
+
requireState(!day?.punches.length, TimesheetsBlock.Changed);
|
|
220
|
+
}
|
|
221
|
+
requireState(!sheet || timesheetIsMutable(sheet), TimesheetsBlock.Locked);
|
|
222
|
+
requireState(!day?.punches.some((p) => p.punch_status === PunchStatusWorking), TimesheetsBlock.Active);
|
|
223
|
+
const open = await this.weeks({
|
|
224
|
+
owner_ids: [opts.owner_id],
|
|
225
|
+
is_archived: false,
|
|
226
|
+
punch_status: PunchStatusWorking,
|
|
227
|
+
});
|
|
228
|
+
requireState(open.length === 0, TimesheetsBlock.Active);
|
|
229
|
+
const result = await this.actions.replaceDay({
|
|
230
|
+
employer_id: opts.employer_id,
|
|
231
|
+
location_id: opts.location_id,
|
|
232
|
+
timezone: opts.timezone,
|
|
233
|
+
punch_in_date: schemaDate(opts.start),
|
|
234
|
+
punch_out_date: schemaDate(opts.end),
|
|
235
|
+
total_pause_minutes: opts.total_pause_minutes,
|
|
236
|
+
total_meal_minutes: opts.total_meal_minutes,
|
|
237
|
+
});
|
|
238
|
+
requireState(!!result.punch_out.id, TimesheetsBlock.Unconfirmed);
|
|
239
|
+
return this.week(result.punch_out.id);
|
|
240
|
+
}
|
|
241
|
+
async removeDay(owner_id, expected, date) {
|
|
242
|
+
const sheet = await this.checkedWeek(expected);
|
|
243
|
+
requireState(sheet.owner_id === owner_id, TimesheetsBlock.Locked);
|
|
244
|
+
const day = sheet.sheets.find((d) => d.week_day === timesheetWeekDay(date));
|
|
245
|
+
requireState(!!day?.punches.length &&
|
|
246
|
+
!day.punches.some((p) => p.punch_status === PunchStatusWorking), TimesheetsBlock.Active);
|
|
247
|
+
const punch = day.punches[0];
|
|
248
|
+
requireState(sameDay(date, punch.punch_in_time), TimesheetsBlock.InvalidDay);
|
|
249
|
+
await this.actions.deleteDay({
|
|
250
|
+
employer_id: sheet.employer_id,
|
|
251
|
+
location_id: punch.location_id,
|
|
252
|
+
timezone: punch.timezone,
|
|
253
|
+
explicit_date: schemaDate(date),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async removeWeek(actor_id, expected) {
|
|
257
|
+
const sheet = await this.checkedWeek(expected);
|
|
258
|
+
requireState(sheet.owner_id === actor_id || sheet.employer_id === actor_id, TimesheetsBlock.Locked);
|
|
259
|
+
requireState(!timesheetHasWorkingPunches(sheet), TimesheetsBlock.Active);
|
|
260
|
+
await this.client.deleteOneWeekSheet({ id: sheet.id });
|
|
261
|
+
}
|
|
262
|
+
async blankWeek(opts) {
|
|
263
|
+
this.validateDay(opts.date, opts.timezone);
|
|
264
|
+
const indexes = await this.periods(opts.employer_id, opts.date);
|
|
265
|
+
const matches = await this.weeks({
|
|
266
|
+
owner_ids: [opts.owner_id],
|
|
267
|
+
employer_ids: [opts.employer_id],
|
|
268
|
+
period_indexes: indexes,
|
|
269
|
+
});
|
|
270
|
+
requireState(matches.length === 0, TimesheetsBlock.Changed);
|
|
271
|
+
const result = await this.actions.createBlankWeekSheet({
|
|
272
|
+
...opts,
|
|
273
|
+
explicit_date: schemaDate(opts.date),
|
|
274
|
+
});
|
|
275
|
+
requireState(!!result.id, TimesheetsBlock.Unconfirmed);
|
|
276
|
+
return this.week(result.id);
|
|
277
|
+
}
|
|
278
|
+
async queryOwners(opts, query, fetch) {
|
|
279
|
+
if (emptyScope(opts))
|
|
280
|
+
return [];
|
|
281
|
+
if (opts.owner_ids === undefined) {
|
|
282
|
+
throw errRequired({
|
|
283
|
+
field: "owner_ids",
|
|
284
|
+
msg: "Owner IDs are required for a timesheet query.",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return await runOwnerScopeQuery({
|
|
288
|
+
owner_ids: opts.owner_ids,
|
|
289
|
+
query,
|
|
290
|
+
signal: opts.signal,
|
|
291
|
+
fetch,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async checkedWeek(expected) {
|
|
295
|
+
const sheet = await this.week(expected.id);
|
|
296
|
+
requireState(sheet.updated_at === expected.updated_at, TimesheetsBlock.Changed);
|
|
297
|
+
requireState(timesheetIsMutable(sheet), TimesheetsBlock.Locked);
|
|
298
|
+
return sheet;
|
|
299
|
+
}
|
|
300
|
+
validateDay(date, timezone) {
|
|
301
|
+
const now = inDateAtTimestamp(this.now().toISOString(), timezone);
|
|
302
|
+
const calendar = new Date(Date.UTC(date.y, date.m - 1, date.d));
|
|
303
|
+
requireState(!!now &&
|
|
304
|
+
date.tz === timezone &&
|
|
305
|
+
date.y > 1 &&
|
|
306
|
+
date.m >= 1 &&
|
|
307
|
+
date.m <= 12 &&
|
|
308
|
+
calendar.getUTCDate() === date.d &&
|
|
309
|
+
Number.isInteger(date.h) &&
|
|
310
|
+
date.h >= 0 &&
|
|
311
|
+
date.h < 24 &&
|
|
312
|
+
Number.isInteger(date.min) &&
|
|
313
|
+
date.min >= 0 &&
|
|
314
|
+
date.min < 60 &&
|
|
315
|
+
wallTime(date) <= wallTime(now), TimesheetsBlock.InvalidDay);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function emptyScope(opts) {
|
|
319
|
+
return [
|
|
320
|
+
opts.owner_ids,
|
|
321
|
+
opts.employer_ids,
|
|
322
|
+
opts.period_indexes,
|
|
323
|
+
opts.location_ids,
|
|
324
|
+
].some((ids) => ids?.length === 0);
|
|
325
|
+
}
|
|
326
|
+
function query(opts) {
|
|
327
|
+
return queryParams({
|
|
328
|
+
employer_id: opts.employer_ids,
|
|
329
|
+
period_index: opts.period_indexes,
|
|
330
|
+
});
|
|
331
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.116",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typescript": "^6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fabriktor/client": "0.0.
|
|
20
|
+
"@fabriktor/client": "0.0.87",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
22
|
"@fabriktor/schema": "0.0.63"
|
|
23
23
|
}
|