@fabriktor/fx 0.0.119 → 0.0.121
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/chat/send-queue.d.ts +4 -2
- package/dist/src/chat/send-queue.js +5 -7
- package/dist/src/core/events.d.ts +13 -0
- package/dist/src/core/events.js +47 -0
- package/dist/src/core/multi-create.d.ts +4 -3
- package/dist/src/core/multi-create.js +7 -17
- package/dist/src/fx.d.ts +3 -0
- package/dist/src/fx.js +3 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/preferences/preferences.d.ts +6 -0
- package/dist/src/preferences/preferences.js +12 -2
- package/dist/src/timesheets/access.d.ts +7 -0
- package/dist/src/timesheets/access.js +10 -3
- package/dist/src/timesheets/timesheets.d.ts +8 -2
- package/dist/src/timesheets/timesheets.js +62 -10
- package/dist/src/timesheets/workspace.d.ts +1 -0
- package/dist/src/timesheets/workspace.js +8 -2
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChatOperator } from "@fabriktor/client";
|
|
2
2
|
import type { Chat, InChat } from "@fabriktor/schema";
|
|
3
|
+
import { type EventListener } from "../core/events.js";
|
|
3
4
|
import { type QueueRetry, type QueueStore, type QueueLock } from "../core/queue.js";
|
|
4
5
|
export declare const ChatSendStatus: {
|
|
5
6
|
readonly Saving: "saving";
|
|
@@ -55,7 +56,8 @@ export declare class ChatSendQueue {
|
|
|
55
56
|
private entries;
|
|
56
57
|
private confirmed;
|
|
57
58
|
private confirmedSequences;
|
|
58
|
-
private
|
|
59
|
+
private readonly events;
|
|
60
|
+
private readonly changed;
|
|
59
61
|
private readonly queue;
|
|
60
62
|
private uploads;
|
|
61
63
|
private confirmations;
|
|
@@ -72,7 +74,7 @@ export declare class ChatSendQueue {
|
|
|
72
74
|
constructor(opts: ChatSendQueueOptions);
|
|
73
75
|
get hasUnsavedMessages(): boolean;
|
|
74
76
|
get snapshot(): ChatSendSnapshot;
|
|
75
|
-
subscribe(listener:
|
|
77
|
+
subscribe(listener: EventListener<ChatSendSnapshot>): () => void;
|
|
76
78
|
start(ownerID: string): Promise<void>;
|
|
77
79
|
pause(): void;
|
|
78
80
|
enqueue(input: InChat, attachments: ChatSendAttachments): string;
|
|
@@ -4,6 +4,7 @@
|
|
|
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
6
|
import { requiredOperationID } from "../core/id.js";
|
|
7
|
+
import { createEvent, EventBus } from "../core/events.js";
|
|
7
8
|
import { PersistentQueue, QueueWakeup, nextQueueRetry, } from "../core/queue.js";
|
|
8
9
|
import { newOptimisticChat } from "./optimistic.js";
|
|
9
10
|
export const ChatSendStatus = {
|
|
@@ -27,7 +28,8 @@ export class ChatSendQueue {
|
|
|
27
28
|
entries = [];
|
|
28
29
|
confirmed = new Map();
|
|
29
30
|
confirmedSequences = new Map();
|
|
30
|
-
|
|
31
|
+
events = new EventBus();
|
|
32
|
+
changed = createEvent();
|
|
31
33
|
queue;
|
|
32
34
|
uploads = new Set();
|
|
33
35
|
confirmations = new Set();
|
|
@@ -59,9 +61,7 @@ export class ChatSendQueue {
|
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
subscribe(listener) {
|
|
62
|
-
this.
|
|
63
|
-
listener(this.snapshot);
|
|
64
|
-
return () => this.listeners.delete(listener);
|
|
64
|
+
return this.events.subscribe(this.changed, listener, () => this.snapshot);
|
|
65
65
|
}
|
|
66
66
|
async start(ownerID) {
|
|
67
67
|
if (this.enabled && this.owner === ownerID) {
|
|
@@ -690,9 +690,7 @@ export class ChatSendQueue {
|
|
|
690
690
|
}
|
|
691
691
|
publish() {
|
|
692
692
|
this.revision++;
|
|
693
|
-
|
|
694
|
-
listener(this.snapshot);
|
|
695
|
-
}
|
|
693
|
+
this.events.publish(this.changed, this.snapshot);
|
|
696
694
|
}
|
|
697
695
|
}
|
|
698
696
|
function chatSendCanConfirm(entry) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare const eventPayload: unique symbol;
|
|
2
|
+
export type EventKey<T> = symbol & {
|
|
3
|
+
readonly [eventPayload]: (value: T) => T;
|
|
4
|
+
};
|
|
5
|
+
export type EventListener<T> = (value: T) => void | Promise<void>;
|
|
6
|
+
export declare function createEvent<T>(): EventKey<T>;
|
|
7
|
+
export declare function notifyObserver(run: () => unknown): void;
|
|
8
|
+
export declare class EventBus {
|
|
9
|
+
private readonly listeners;
|
|
10
|
+
subscribe<T>(event: EventKey<T>, listener: EventListener<NoInfer<T>>, initial?: () => T): () => void;
|
|
11
|
+
publish<T>(event: EventKey<T>, value: NoInfer<T>): void;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (C) Fabriktor, Inc. 2025-present.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
// not use this file except in compliance with the License. You may obtain
|
|
5
|
+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
export function createEvent() {
|
|
7
|
+
return Symbol();
|
|
8
|
+
}
|
|
9
|
+
export function notifyObserver(run) {
|
|
10
|
+
try {
|
|
11
|
+
void Promise.resolve(run()).catch(observerError);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
observerError(err);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function observerError(err) {
|
|
18
|
+
console.error("Event observer failed.", err);
|
|
19
|
+
}
|
|
20
|
+
export class EventBus {
|
|
21
|
+
listeners = new Map();
|
|
22
|
+
subscribe(event, listener, initial) {
|
|
23
|
+
let listeners = this.listeners.get(event);
|
|
24
|
+
if (!listeners) {
|
|
25
|
+
listeners = new Set();
|
|
26
|
+
this.listeners.set(event, listeners);
|
|
27
|
+
}
|
|
28
|
+
const observer = listener;
|
|
29
|
+
listeners.add(observer);
|
|
30
|
+
if (initial)
|
|
31
|
+
notifyObserver(() => listener(initial()));
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(observer);
|
|
34
|
+
if (!listeners.size && this.listeners.get(event) === listeners)
|
|
35
|
+
this.listeners.delete(event);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
publish(event, value) {
|
|
39
|
+
const listeners = this.listeners.get(event);
|
|
40
|
+
if (!listeners)
|
|
41
|
+
return;
|
|
42
|
+
for (const listener of [...listeners]) {
|
|
43
|
+
if (listeners.has(listener))
|
|
44
|
+
notifyObserver(() => listener(value));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EventListener } from "./events.js";
|
|
1
2
|
export declare const multiCreateMaximumEntries = 40;
|
|
2
3
|
export declare const multiCreateConcurrency = 5;
|
|
3
4
|
export declare const MultiCreateStatus: {
|
|
@@ -52,13 +53,14 @@ export type MultiCreateOptions<Input, Result> = {
|
|
|
52
53
|
export declare class MultiCreator<Input, Result> {
|
|
53
54
|
private readonly opts;
|
|
54
55
|
private entries;
|
|
55
|
-
private
|
|
56
|
+
private readonly events;
|
|
57
|
+
private readonly changed;
|
|
56
58
|
private running;
|
|
57
59
|
private controller;
|
|
58
60
|
private completion;
|
|
59
61
|
constructor(opts: MultiCreateOptions<Input, Result>);
|
|
60
62
|
get snapshot(): MultiCreateSnapshot<Input, Result>;
|
|
61
|
-
subscribe(listener:
|
|
63
|
+
subscribe(listener: EventListener<MultiCreateSnapshot<Input, Result>>): () => void;
|
|
62
64
|
add(input: Input): string;
|
|
63
65
|
edit(id: string, input: Input): boolean;
|
|
64
66
|
remove(id: string): boolean;
|
|
@@ -70,5 +72,4 @@ export declare class MultiCreator<Input, Result> {
|
|
|
70
72
|
private run;
|
|
71
73
|
private replace;
|
|
72
74
|
private publish;
|
|
73
|
-
private notify;
|
|
74
75
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
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 { createEvent, EventBus, notifyObserver, } from "./events.js";
|
|
6
7
|
export const multiCreateMaximumEntries = 40;
|
|
7
8
|
export const multiCreateConcurrency = 5;
|
|
8
9
|
export const MultiCreateStatus = {
|
|
@@ -47,7 +48,8 @@ const sharedMultiCreatePool = new MultiCreatePool();
|
|
|
47
48
|
export class MultiCreator {
|
|
48
49
|
opts;
|
|
49
50
|
entries = [];
|
|
50
|
-
|
|
51
|
+
events = new EventBus();
|
|
52
|
+
changed = createEvent();
|
|
51
53
|
running = false;
|
|
52
54
|
controller = new AbortController();
|
|
53
55
|
completion = Promise.resolve();
|
|
@@ -58,9 +60,7 @@ export class MultiCreator {
|
|
|
58
60
|
return { entries: this.entries, running: this.running };
|
|
59
61
|
}
|
|
60
62
|
subscribe(listener) {
|
|
61
|
-
this.
|
|
62
|
-
listener(this.snapshot);
|
|
63
|
-
return () => this.listeners.delete(listener);
|
|
63
|
+
return this.events.subscribe(this.changed, listener, () => this.snapshot);
|
|
64
64
|
}
|
|
65
65
|
add(input) {
|
|
66
66
|
if (this.running || this.entries.length >= multiCreateMaximumEntries)
|
|
@@ -170,14 +170,14 @@ export class MultiCreator {
|
|
|
170
170
|
catch (error) {
|
|
171
171
|
current = { ...current, status: MultiCreateStatus.Failed, error };
|
|
172
172
|
this.replace(current);
|
|
173
|
-
|
|
173
|
+
notifyObserver(() => this.opts.on_failed?.(current));
|
|
174
174
|
}
|
|
175
175
|
})));
|
|
176
176
|
this.running = false;
|
|
177
177
|
this.publish();
|
|
178
178
|
const ids = new Set(pending.map((entry) => entry.id));
|
|
179
179
|
const entries = this.entries.filter((entry) => ids.has(entry.id));
|
|
180
|
-
|
|
180
|
+
notifyObserver(() => this.opts.on_settled?.({
|
|
181
181
|
entries,
|
|
182
182
|
created: entries.filter((entry) => entry.status === MultiCreateStatus.Created).length,
|
|
183
183
|
failed: entries.filter((entry) => entry.status === MultiCreateStatus.Failed).length,
|
|
@@ -188,16 +188,6 @@ export class MultiCreator {
|
|
|
188
188
|
this.publish();
|
|
189
189
|
}
|
|
190
190
|
publish() {
|
|
191
|
-
|
|
192
|
-
this.notify(() => listener(this.snapshot));
|
|
193
|
-
}
|
|
194
|
-
notify(run) {
|
|
195
|
-
// Presentation errors cannot turn a successful creation into a failed POST.
|
|
196
|
-
try {
|
|
197
|
-
run();
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
console.error("Multi-creation observer failed.", err);
|
|
201
|
-
}
|
|
191
|
+
this.events.publish(this.changed, this.snapshot);
|
|
202
192
|
}
|
|
203
193
|
}
|
package/dist/src/fx.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { BillableFX, BillableFXOperator } from "./billable/billable.js";
|
|
|
4
4
|
import type { CallsFX, CallsFXOperator } from "./calls/calls.js";
|
|
5
5
|
import type { ChatFX, ChatFXOperator } from "./chat/chat.js";
|
|
6
6
|
import type { ContactsFX, ContactsFXOperator } from "./contacts/contacts.js";
|
|
7
|
+
import { EventBus } from "./core/events.js";
|
|
7
8
|
import type { FeedFX, FeedFXOperator } from "./feed/feed.js";
|
|
8
9
|
import type { FulfillmentsFX, FulfillmentsFXOperator } from "./fulfillments/fulfillments.js";
|
|
9
10
|
import type { JobsFX, JobsFXOperator } from "./jobs/jobs.js";
|
|
@@ -56,6 +57,7 @@ export type DefaultFXOptions = {
|
|
|
56
57
|
marketplace_keep_alive_interval_ms: number;
|
|
57
58
|
};
|
|
58
59
|
export interface FXOperator {
|
|
60
|
+
readonly events: EventBus;
|
|
59
61
|
billable: BillableFXOperator;
|
|
60
62
|
calls: CallsFXOperator;
|
|
61
63
|
chat: ChatFXOperator;
|
|
@@ -78,6 +80,7 @@ export interface FXOperator {
|
|
|
78
80
|
users: UsersFXOperator;
|
|
79
81
|
}
|
|
80
82
|
export declare class FX implements FXOperator {
|
|
83
|
+
readonly events: EventBus;
|
|
81
84
|
billable: BillableFX;
|
|
82
85
|
calls: CallsFX;
|
|
83
86
|
chat: ChatFX;
|
package/dist/src/fx.js
CHANGED
|
@@ -8,6 +8,7 @@ import { newBillableFX } from "./billable/billable.js";
|
|
|
8
8
|
import { newCallsFX } from "./calls/calls.js";
|
|
9
9
|
import { newChatFX } from "./chat/chat.js";
|
|
10
10
|
import { newContactsFX } from "./contacts/contacts.js";
|
|
11
|
+
import { EventBus } from "./core/events.js";
|
|
11
12
|
import { newFeedFX } from "./feed/feed.js";
|
|
12
13
|
import { newFulfillmentsFX } from "./fulfillments/fulfillments.js";
|
|
13
14
|
import { newJobsFX } from "./jobs/jobs.js";
|
|
@@ -25,6 +26,7 @@ import { newStorageFX } from "./storage/storage.js";
|
|
|
25
26
|
import { newTimesheetsFX } from "./timesheets/timesheets.js";
|
|
26
27
|
import { newUsersFX } from "./users/users.js";
|
|
27
28
|
export class FX {
|
|
29
|
+
events = new EventBus();
|
|
28
30
|
billable;
|
|
29
31
|
calls;
|
|
30
32
|
chat;
|
|
@@ -83,6 +85,7 @@ export class FX {
|
|
|
83
85
|
});
|
|
84
86
|
this.preferences = newPreferencesFX({
|
|
85
87
|
preferences: opts.preferences,
|
|
88
|
+
events: this.events,
|
|
86
89
|
});
|
|
87
90
|
this.privacy = newPrivacyFX({
|
|
88
91
|
privacy: opts.privacy,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./contacts/contacts.js";
|
|
|
8
8
|
export * from "./contacts/scope.js";
|
|
9
9
|
export * from "./core/date.js";
|
|
10
10
|
export * from "./core/err.js";
|
|
11
|
+
export * from "./core/events.js";
|
|
11
12
|
export * from "./core/id.js";
|
|
12
13
|
export * from "./core/multi-create.js";
|
|
13
14
|
export * from "./core/pre-action.js";
|
package/dist/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./contacts/contacts.js";
|
|
|
8
8
|
export * from "./contacts/scope.js";
|
|
9
9
|
export * from "./core/date.js";
|
|
10
10
|
export * from "./core/err.js";
|
|
11
|
+
export * from "./core/events.js";
|
|
11
12
|
export * from "./core/id.js";
|
|
12
13
|
export * from "./core/multi-create.js";
|
|
13
14
|
export * from "./core/pre-action.js";
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { PreferencesOperator } from "@fabriktor/client";
|
|
2
2
|
import { type InPrefClient, type InPrefCompany, type InPrefEmployee, type OperationResult, type PrefPayment, type PrefClient, type PrefEmployee, type PrefCompany, type PrefVisibility, type PrefLanguage, type PrefNotif, type PrefEmail, type PrefTutorial } from "@fabriktor/schema";
|
|
3
|
+
import { EventBus } from "../core/events.js";
|
|
4
|
+
export declare const PreferencesEvents: {
|
|
5
|
+
readonly CompanySaved: import("../index.js").EventKey<PrefCompany>;
|
|
6
|
+
};
|
|
3
7
|
type PrefPaymentInsertOptions = Parameters<PreferencesOperator["insertOnePrefPayment"]>[0];
|
|
4
8
|
type PrefPaymentReplaceOptions = Parameters<PreferencesOperator["replaceOnePrefPayment"]>[0];
|
|
5
9
|
type PrefClientInsertOptions = Parameters<PreferencesOperator["insertOnePrefClient"]>[0];
|
|
@@ -112,9 +116,11 @@ export interface PreferencesFXOperator {
|
|
|
112
116
|
}
|
|
113
117
|
export type PreferencesFXOptions = {
|
|
114
118
|
preferences: PreferencesOperator;
|
|
119
|
+
events?: EventBus;
|
|
115
120
|
};
|
|
116
121
|
export declare class PreferencesFX implements PreferencesFXOperator {
|
|
117
122
|
private preferences;
|
|
123
|
+
private readonly events;
|
|
118
124
|
constructor(opts: PreferencesFXOptions);
|
|
119
125
|
createPrefPayment(opts: CreatePrefPaymentOptions): Promise<PrefPayment>;
|
|
120
126
|
replacePrefPayment(opts: ReplacePrefPaymentOptions): Promise<PrefPayment>;
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import { queryParam } from "@fabriktor/client";
|
|
7
7
|
import { OwnerIDKey, } from "@fabriktor/schema";
|
|
8
8
|
import { errResolveFailed } from "../core/err.js";
|
|
9
|
+
import { createEvent, EventBus } from "../core/events.js";
|
|
9
10
|
import { requiredOperationID } from "../core/id.js";
|
|
11
|
+
export const PreferencesEvents = {
|
|
12
|
+
CompanySaved: createEvent(),
|
|
13
|
+
};
|
|
10
14
|
const resourcePrefPayment = "pref_payment";
|
|
11
15
|
const msgCreatedPrefPaymentIDMissing = "Created preference payment response did not contain an ID.";
|
|
12
16
|
const msgPrefPaymentMissing = "Preference payment could not be resolved after the mutation.";
|
|
@@ -40,8 +44,10 @@ export const PrefCompanyDocumentNote = {
|
|
|
40
44
|
};
|
|
41
45
|
export class PreferencesFX {
|
|
42
46
|
preferences;
|
|
47
|
+
events;
|
|
43
48
|
constructor(opts) {
|
|
44
49
|
this.preferences = opts.preferences;
|
|
50
|
+
this.events = opts.events ?? new EventBus();
|
|
45
51
|
}
|
|
46
52
|
async createPrefPayment(opts) {
|
|
47
53
|
const out = await this.preferences.insertOnePrefPayment(opts);
|
|
@@ -102,11 +108,15 @@ export class PreferencesFX {
|
|
|
102
108
|
async createPrefCompany(opts) {
|
|
103
109
|
const out = await this.preferences.insertOnePrefCompany(opts);
|
|
104
110
|
const id = requiredOperationID(out, msgCreatedPrefCompanyIDMissing);
|
|
105
|
-
|
|
111
|
+
const pref = await this.findPrefCompany(id);
|
|
112
|
+
this.events.publish(PreferencesEvents.CompanySaved, pref);
|
|
113
|
+
return pref;
|
|
106
114
|
}
|
|
107
115
|
async replacePrefCompany(opts) {
|
|
108
116
|
await this.preferences.replaceOnePrefCompany(opts);
|
|
109
|
-
|
|
117
|
+
const pref = await this.findPrefCompany(opts.id);
|
|
118
|
+
this.events.publish(PreferencesEvents.CompanySaved, pref);
|
|
119
|
+
return pref;
|
|
110
120
|
}
|
|
111
121
|
async getPrefCompany(opts) {
|
|
112
122
|
const pref = await this.queryPrefCompany(opts.owner_id);
|
|
@@ -13,9 +13,16 @@ export type QueryTimesheetSyncDestinationsOptions = {
|
|
|
13
13
|
source: WeekSheet;
|
|
14
14
|
signal?: AbortSignal;
|
|
15
15
|
};
|
|
16
|
+
export type QueryTimesheetSyncSourcesOptions = Pick<TimesheetsReadOptions, "context" | "owner_ids" | "period_indexes" | "signal">;
|
|
17
|
+
export type TimesheetSyncTarget = {
|
|
18
|
+
owner_id: string;
|
|
19
|
+
week_sheet?: WeekSheet;
|
|
20
|
+
};
|
|
16
21
|
type TimesheetRecord = WeekSheet | Summary;
|
|
17
22
|
export declare function timesheetCanReadResource(context: TimesheetsActor): boolean;
|
|
18
23
|
export declare function timesheetCanCreateWeek(context: TimesheetsActor): boolean;
|
|
24
|
+
export declare function timesheetCanManageSync(context: TimesheetsActor): boolean;
|
|
25
|
+
export declare function timesheetCanSyncRecord(context: TimesheetsActor, record: WeekSheet | null | undefined): boolean;
|
|
19
26
|
export declare function timesheetCanReadRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
|
|
20
27
|
export declare function timesheetCanWriteRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
|
|
21
28
|
export declare function timesheetCanDeleteRecord(context: TimesheetsActor, record: TimesheetRecord | null | undefined): boolean;
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { PunchStatusWorking, UserTypeEmployee, } from "@fabriktor/schema";
|
|
1
|
+
import { PunchStatusWorking, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
|
|
2
2
|
import { employmentActivityOwnerIDs, employmentCanRead, employmentEmployeeIDs, employmentEmployerIDs, employmentManagesRecord, employmentOwnsRecord, } from "../contacts/scope.js";
|
|
3
3
|
import { restrictOwnerIDs } from "../core/scope.js";
|
|
4
|
-
import { timesheetCanApprove, timesheetCanSync, timesheetHasWorkingPunches, timesheetIsMutable, } from "./workspace.js";
|
|
4
|
+
import { timesheetCanApprove, timesheetCanSync, timesheetCanSyncSource, timesheetHasWorkingPunches, timesheetIsMutable, } from "./workspace.js";
|
|
5
5
|
export function timesheetCanReadResource(context) {
|
|
6
6
|
return employmentCanRead(context);
|
|
7
7
|
}
|
|
8
8
|
export function timesheetCanCreateWeek(context) {
|
|
9
9
|
return timesheetCanReadResource(context) && context.active_mode === UserTypeEmployee;
|
|
10
10
|
}
|
|
11
|
+
export function timesheetCanManageSync(context) {
|
|
12
|
+
return timesheetCanReadResource(context) && context.active_mode === UserTypeEmployer;
|
|
13
|
+
}
|
|
14
|
+
export function timesheetCanSyncRecord(context, record) {
|
|
15
|
+
return record != null && timesheetCanManageSync(context) &&
|
|
16
|
+
employmentManagesRecord(context, record) && timesheetCanSyncSource(record);
|
|
17
|
+
}
|
|
11
18
|
export function timesheetCanReadRecord(context, record) {
|
|
12
19
|
return timesheetCanReadResource(context) && record != null && (employmentOwnsRecord(context, record) || employmentManagesRecord(context, record));
|
|
13
20
|
}
|
|
@@ -30,7 +37,7 @@ export function timesheetCanEditDay(context, record, week_day) {
|
|
|
30
37
|
day.punches.every((punch) => punch.punch_status !== PunchStatusWorking);
|
|
31
38
|
}
|
|
32
39
|
export function timesheetCanSyncDestination(context, source, destination) {
|
|
33
|
-
return
|
|
40
|
+
return timesheetCanSyncRecord(context, source) &&
|
|
34
41
|
timesheetCanWriteRecord(context, destination) && timesheetCanSync(source, destination);
|
|
35
42
|
}
|
|
36
43
|
export function timesheetEmployeeScopeIDs(context) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Date as SchemaDate, type OperationResult, type Summary, type WeekSheet } from "@fabriktor/schema";
|
|
2
2
|
import type { BillableOperator, TimesheetsOperator } from "@fabriktor/client";
|
|
3
3
|
import type { ContactsFXOperator } from "../contacts/contacts.js";
|
|
4
|
-
import { type LoadTimesheetsReadContextOptions, type QueryTimesheetSyncDestinationsOptions, type TimesheetsReadContext, type TimesheetsReadOptions } from "./access.js";
|
|
4
|
+
import { type LoadTimesheetsReadContextOptions, type QueryTimesheetSyncDestinationsOptions, type QueryTimesheetSyncSourcesOptions, type TimesheetsReadContext, type TimesheetsReadOptions, type TimesheetSyncTarget } from "./access.js";
|
|
5
5
|
import { TimesheetsWorkspace } from "./workspace.js";
|
|
6
6
|
type PunchPayload = Parameters<TimesheetsOperator["punch"]>[0];
|
|
7
7
|
type WeekSheetReplaceOptions = Parameters<TimesheetsOperator["replaceOneWeekSheet"]>[0];
|
|
@@ -57,7 +57,9 @@ export type PunchControls = {
|
|
|
57
57
|
};
|
|
58
58
|
export type SyncWeekOptions = {
|
|
59
59
|
from: WeekSheet;
|
|
60
|
-
to
|
|
60
|
+
to?: WeekSheet[];
|
|
61
|
+
to_owner_ids?: string[];
|
|
62
|
+
actor?: LoadTimesheetsReadContextOptions;
|
|
61
63
|
};
|
|
62
64
|
export type SyncDaysOptions = SyncWeekOptions & {
|
|
63
65
|
days: number[];
|
|
@@ -72,6 +74,8 @@ export interface TimesheetsFXOperator {
|
|
|
72
74
|
queryReadableWeekSheets(opts: TimesheetsReadOptions): Promise<WeekSheet[]>;
|
|
73
75
|
queryReadableSummaries(opts: TimesheetsReadOptions): Promise<Summary[]>;
|
|
74
76
|
querySyncDestinations(opts: QueryTimesheetSyncDestinationsOptions): Promise<WeekSheet[]>;
|
|
77
|
+
querySyncSources(opts: QueryTimesheetSyncSourcesOptions): Promise<WeekSheet[]>;
|
|
78
|
+
querySyncTargets(opts: QueryTimesheetSyncDestinationsOptions): Promise<TimesheetSyncTarget[]>;
|
|
75
79
|
replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
|
|
76
80
|
replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
|
|
77
81
|
punchControls(opts: PunchControlsOptions): PunchControls;
|
|
@@ -96,6 +100,8 @@ export declare class TimesheetsFX implements TimesheetsFXOperator {
|
|
|
96
100
|
queryReadableWeekSheets(opts: TimesheetsReadOptions): Promise<WeekSheet[]>;
|
|
97
101
|
queryReadableSummaries(opts: TimesheetsReadOptions): Promise<Summary[]>;
|
|
98
102
|
querySyncDestinations(opts: QueryTimesheetSyncDestinationsOptions): Promise<WeekSheet[]>;
|
|
103
|
+
querySyncSources(opts: QueryTimesheetSyncSourcesOptions): Promise<WeekSheet[]>;
|
|
104
|
+
querySyncTargets(opts: QueryTimesheetSyncDestinationsOptions): Promise<TimesheetSyncTarget[]>;
|
|
99
105
|
replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
|
|
100
106
|
replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
|
|
101
107
|
punchControls(opts: PunchControlsOptions): PunchControls;
|
|
@@ -9,8 +9,8 @@ import { errRequired, errResolveFailed, errValidation, } from "../core/err.js";
|
|
|
9
9
|
import { normalizeIDs } from "../core/id.js";
|
|
10
10
|
import { loadEmploymentReadContext } from "../contacts/scope.js";
|
|
11
11
|
import { PreActionBlocked } from "../core/pre-action.js";
|
|
12
|
-
import {
|
|
13
|
-
import { TimesheetsWorkspace, TimesheetsBlock, timesheetCanApprove, timesheetCanSync, timesheetIsMutable, } from "./workspace.js";
|
|
12
|
+
import { timesheetCanSyncRecord, timesheetCanSyncDestination, timesheetEmployeeScopeIDs, timesheetReadFilters, } from "./access.js";
|
|
13
|
+
import { TimesheetsWorkspace, TimesheetsBlock, timesheetCanApprove, timesheetCanSync, timesheetCanSyncSource, timesheetIsMutable, } from "./workspace.js";
|
|
14
14
|
const allWeekDays = [0, 1, 2, 3, 4, 5, 6];
|
|
15
15
|
const minuteStep = 15;
|
|
16
16
|
const msgPunchInProgress = "A punch action is already in progress.";
|
|
@@ -55,7 +55,7 @@ export class TimesheetsFX {
|
|
|
55
55
|
const source = await this.workspace.week(opts.source.id, opts.signal);
|
|
56
56
|
if (source.updated_at !== opts.source.updated_at)
|
|
57
57
|
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
58
|
-
if (!
|
|
58
|
+
if (!timesheetCanSyncRecord(opts.context, source))
|
|
59
59
|
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
60
60
|
const weeks = await this.queryReadableWeekSheets({
|
|
61
61
|
context: opts.context,
|
|
@@ -66,6 +66,41 @@ export class TimesheetsFX {
|
|
|
66
66
|
});
|
|
67
67
|
return weeks.filter((week) => timesheetCanSyncDestination(opts.context, source, week));
|
|
68
68
|
}
|
|
69
|
+
async querySyncSources(opts) {
|
|
70
|
+
// Full weeks are needed for eligibility; never use cleaned day/punch results.
|
|
71
|
+
const weeks = await this.queryReadableWeekSheets({
|
|
72
|
+
context: opts.context,
|
|
73
|
+
owner_ids: opts.owner_ids,
|
|
74
|
+
period_indexes: opts.period_indexes,
|
|
75
|
+
signal: opts.signal,
|
|
76
|
+
});
|
|
77
|
+
return weeks.filter((week) => timesheetCanSyncRecord(opts.context, week));
|
|
78
|
+
}
|
|
79
|
+
async querySyncTargets(opts) {
|
|
80
|
+
const source = await this.workspace.week(opts.source.id, opts.signal);
|
|
81
|
+
if (source.updated_at !== opts.source.updated_at)
|
|
82
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
83
|
+
if (!timesheetCanSyncRecord(opts.context, source))
|
|
84
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
85
|
+
// Include locked records: hiding them would mistake them for missing weeks.
|
|
86
|
+
const weeks = await this.queryReadableWeekSheets({
|
|
87
|
+
context: opts.context,
|
|
88
|
+
employer_ids: [source.employer_id],
|
|
89
|
+
period_indexes: [source.period_index],
|
|
90
|
+
signal: opts.signal,
|
|
91
|
+
});
|
|
92
|
+
const targets = [];
|
|
93
|
+
for (const owner_id of timesheetEmployeeScopeIDs(opts.context)) {
|
|
94
|
+
if (owner_id === source.owner_id)
|
|
95
|
+
continue;
|
|
96
|
+
const matches = weeks.filter((week) => week.owner_id === owner_id);
|
|
97
|
+
if (matches.length === 0)
|
|
98
|
+
targets.push({ owner_id });
|
|
99
|
+
else if (matches.length === 1 && timesheetCanSyncDestination(opts.context, source, matches[0]))
|
|
100
|
+
targets.push({ owner_id, week_sheet: matches[0] });
|
|
101
|
+
}
|
|
102
|
+
return targets;
|
|
103
|
+
}
|
|
69
104
|
async replaceWeekSheet(opts) {
|
|
70
105
|
await this.timesheets.replaceOneWeekSheet(opts);
|
|
71
106
|
return await this.findWeekSheet(opts.id);
|
|
@@ -212,8 +247,7 @@ export class TimesheetsFX {
|
|
|
212
247
|
}
|
|
213
248
|
async syncWeek(opts) {
|
|
214
249
|
await this.syncSheets({
|
|
215
|
-
|
|
216
|
-
to: opts.to,
|
|
250
|
+
...opts,
|
|
217
251
|
days: [...allWeekDays],
|
|
218
252
|
});
|
|
219
253
|
}
|
|
@@ -226,8 +260,7 @@ export class TimesheetsFX {
|
|
|
226
260
|
});
|
|
227
261
|
}
|
|
228
262
|
await this.syncSheets({
|
|
229
|
-
|
|
230
|
-
to: opts.to,
|
|
263
|
+
...opts,
|
|
231
264
|
days,
|
|
232
265
|
});
|
|
233
266
|
}
|
|
@@ -308,18 +341,36 @@ export class TimesheetsFX {
|
|
|
308
341
|
const source = await this.findWeekSheet(opts.from.id);
|
|
309
342
|
if (source.updated_at !== opts.from.updated_at)
|
|
310
343
|
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
311
|
-
if (!
|
|
344
|
+
if (!timesheetCanSyncSource(source))
|
|
312
345
|
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
313
346
|
opts = { ...opts, from: source };
|
|
314
347
|
const from_id = requiredID(opts.from.id, "from", msgWeekSheetIDRequired);
|
|
315
|
-
|
|
348
|
+
const owner_ids = normalizeIDs(opts.to_owner_ids ?? []);
|
|
349
|
+
if (!opts.to?.length && owner_ids.length === 0) {
|
|
316
350
|
throw errRequired({
|
|
317
351
|
field: "to",
|
|
318
352
|
msg: msgDestinationWeekSheetsRequired,
|
|
319
353
|
});
|
|
320
354
|
}
|
|
355
|
+
if (owner_ids.length > 0) {
|
|
356
|
+
if (!opts.actor)
|
|
357
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
358
|
+
const context = await this.loadReadContext(opts.actor);
|
|
359
|
+
const targets = await this.querySyncTargets({ context, source });
|
|
360
|
+
if (owner_ids.some((id) => !targets.some((target) => target.owner_id === id)))
|
|
361
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
362
|
+
// Refetch full records immediately before submission. The service checks
|
|
363
|
+
// again inside its transaction and creates missing destinations there.
|
|
364
|
+
for (const target of targets) {
|
|
365
|
+
if (!owner_ids.includes(target.owner_id) || !target.week_sheet)
|
|
366
|
+
continue;
|
|
367
|
+
const fresh = await this.findWeekSheet(target.week_sheet.id);
|
|
368
|
+
if (!timesheetCanSync(source, fresh))
|
|
369
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
321
372
|
const to_ids = [];
|
|
322
|
-
for (const expected of opts.to) {
|
|
373
|
+
for (const expected of opts.to ?? []) {
|
|
323
374
|
const week_sheet = await this.findWeekSheet(expected.id);
|
|
324
375
|
if (week_sheet.updated_at !== expected.updated_at)
|
|
325
376
|
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
@@ -337,6 +388,7 @@ export class TimesheetsFX {
|
|
|
337
388
|
const input = {
|
|
338
389
|
from_id,
|
|
339
390
|
to_ids: normalizeIDs(to_ids),
|
|
391
|
+
to_owner_ids: owner_ids,
|
|
340
392
|
days: normalizeDays(opts.days),
|
|
341
393
|
};
|
|
342
394
|
await this.timesheets.sync(input);
|
|
@@ -50,6 +50,7 @@ export declare function timesheetPunches(sheet: WeekSheet): Punch[];
|
|
|
50
50
|
export declare function timesheetHasWorkingPunches(sheet: WeekSheet): boolean;
|
|
51
51
|
export declare function timesheetIsMutable(sheet: WeekSheet): boolean;
|
|
52
52
|
export declare function timesheetCanApprove(sheet: WeekSheet): boolean;
|
|
53
|
+
export declare function timesheetCanSyncSource(sheet: WeekSheet): boolean;
|
|
53
54
|
export declare function timesheetCanSync(source: WeekSheet, destination: WeekSheet): boolean;
|
|
54
55
|
export declare function timesheetStateKey(state: TimesheetPunchState): string;
|
|
55
56
|
export declare function timesheetWeekDay(date: InDate): number;
|
|
@@ -32,12 +32,18 @@ export function timesheetCanApprove(sheet) {
|
|
|
32
32
|
punches.length > 0 &&
|
|
33
33
|
punches.every((p) => p.punch_status === PunchStatusPending));
|
|
34
34
|
}
|
|
35
|
+
export function timesheetCanSyncSource(sheet) {
|
|
36
|
+
const punches = timesheetPunches(sheet);
|
|
37
|
+
return punches.length > 0 && punches.every((punch) => punch.punch_status === PunchStatusPending || punch.punch_status === PunchStatusApproved);
|
|
38
|
+
}
|
|
35
39
|
export function timesheetCanSync(source, destination) {
|
|
36
|
-
return
|
|
37
|
-
|
|
40
|
+
return timesheetCanSyncSource(source) && !destination.is_archived &&
|
|
41
|
+
timesheetPunches(destination).every((punch) => punch.punch_status === PunchStatusPending) &&
|
|
38
42
|
destination.employer_id === source.employer_id &&
|
|
39
43
|
destination.id !== source.id &&
|
|
40
44
|
destination.period_index === source.period_index &&
|
|
45
|
+
destination.period === source.period &&
|
|
46
|
+
destination.period_year === source.period_year &&
|
|
41
47
|
destination.timezone === source.timezone;
|
|
42
48
|
}
|
|
43
49
|
export function timesheetStateKey(state) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.121",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"typescript": "^6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fabriktor/client": "0.0.
|
|
20
|
+
"@fabriktor/client": "0.0.90",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
|
-
"@fabriktor/schema": "0.0.
|
|
22
|
+
"@fabriktor/schema": "0.0.65"
|
|
23
23
|
}
|
|
24
24
|
}
|