@fabriktor/fx 0.0.113 → 0.0.115
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/fx.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/timesheets/timesheets.d.ts +6 -1
- package/dist/src/timesheets/timesheets.js +28 -3
- package/dist/src/timesheets/workspace.d.ts +76 -0
- package/dist/src/timesheets/workspace.js +322 -0
- package/package.json +2 -2
package/dist/src/fx.js
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from "./routes/routes.js";
|
|
|
31
31
|
export * from "./session/session.js";
|
|
32
32
|
export * from "./storage/storage.js";
|
|
33
33
|
export * from "./timesheets/timesheets.js";
|
|
34
|
+
export * from "./timesheets/workspace.js";
|
|
34
35
|
export * from "./users/email.js";
|
|
35
36
|
export * from "./users/phone.js";
|
|
36
37
|
export * from "./users/username.js";
|
package/dist/src/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export * from "./routes/routes.js";
|
|
|
31
31
|
export * from "./session/session.js";
|
|
32
32
|
export * from "./storage/storage.js";
|
|
33
33
|
export * from "./timesheets/timesheets.js";
|
|
34
|
+
export * from "./timesheets/workspace.js";
|
|
34
35
|
export * from "./users/email.js";
|
|
35
36
|
export * from "./users/phone.js";
|
|
36
37
|
export * from "./users/username.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
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 { TimesheetsWorkspace } from "./workspace.js";
|
|
3
4
|
type PunchPayload = Parameters<TimesheetsOperator["punch"]>[0];
|
|
4
5
|
type WeekSheetReplaceOptions = Parameters<TimesheetsOperator["replaceOneWeekSheet"]>[0];
|
|
5
6
|
type SummaryReplaceOptions = Parameters<TimesheetsOperator["replaceOneSummary"]>[0];
|
|
@@ -7,6 +8,8 @@ export type ReplaceWeekSheetOptions = WeekSheetReplaceOptions;
|
|
|
7
8
|
export type ReplaceSummaryOptions = SummaryReplaceOptions;
|
|
8
9
|
export type TimesheetsFXOptions = {
|
|
9
10
|
timesheets: TimesheetsOperator;
|
|
11
|
+
billable?: BillableOperator;
|
|
12
|
+
now?: () => globalThis.Date;
|
|
10
13
|
};
|
|
11
14
|
export type PunchContext = Pick<PunchPayload, "timezone" | "location_id" | "employer_id">;
|
|
12
15
|
export type PunchInOptions = PunchContext & {
|
|
@@ -61,6 +64,7 @@ export type ApproveWeekSheetsOptions = {
|
|
|
61
64
|
week_sheets: WeekSheet[];
|
|
62
65
|
};
|
|
63
66
|
export interface TimesheetsFXOperator {
|
|
67
|
+
readonly workspace: TimesheetsWorkspace;
|
|
64
68
|
replaceWeekSheet(opts: ReplaceWeekSheetOptions): Promise<WeekSheet>;
|
|
65
69
|
replaceSummary(opts: ReplaceSummaryOptions): Promise<Summary>;
|
|
66
70
|
punchControls(opts: PunchControlsOptions): PunchControls;
|
|
@@ -76,6 +80,7 @@ export interface TimesheetsFXOperator {
|
|
|
76
80
|
approveWeekSheets(opts: ApproveWeekSheetsOptions): Promise<void>;
|
|
77
81
|
}
|
|
78
82
|
export declare class TimesheetsFX implements TimesheetsFXOperator {
|
|
83
|
+
readonly workspace: TimesheetsWorkspace;
|
|
79
84
|
private timesheets;
|
|
80
85
|
private punch_in_progress;
|
|
81
86
|
constructor(opts: TimesheetsFXOptions);
|
|
@@ -6,6 +6,8 @@
|
|
|
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 { PreActionBlocked } from "../core/pre-action.js";
|
|
10
|
+
import { TimesheetsWorkspace, TimesheetsBlock, timesheetCanApprove, timesheetIsMutable, timesheetPunches, } from "./workspace.js";
|
|
9
11
|
const allWeekDays = [0, 1, 2, 3, 4, 5, 6];
|
|
10
12
|
const minuteStep = 15;
|
|
11
13
|
const msgPunchInProgress = "A punch action is already in progress.";
|
|
@@ -27,11 +29,13 @@ const resourceSummary = "summary";
|
|
|
27
29
|
const msgWeekSheetMissing = "Week sheet could not be resolved after the mutation.";
|
|
28
30
|
const msgSummaryMissing = "Summary could not be resolved after the mutation.";
|
|
29
31
|
export class TimesheetsFX {
|
|
32
|
+
workspace;
|
|
30
33
|
timesheets;
|
|
31
34
|
punch_in_progress;
|
|
32
35
|
constructor(opts) {
|
|
33
36
|
this.timesheets = opts.timesheets;
|
|
34
37
|
this.punch_in_progress = false;
|
|
38
|
+
this.workspace = new TimesheetsWorkspace(opts.timesheets, this, opts.billable, opts.now);
|
|
35
39
|
}
|
|
36
40
|
async replaceWeekSheet(opts) {
|
|
37
41
|
await this.timesheets.replaceOneWeekSheet(opts);
|
|
@@ -43,7 +47,7 @@ export class TimesheetsFX {
|
|
|
43
47
|
}
|
|
44
48
|
punchControls(opts) {
|
|
45
49
|
validateWeekDay(opts.week_day, "week_day");
|
|
46
|
-
if (opts.week_sheet
|
|
50
|
+
if (opts.week_sheet && !timesheetIsMutable(opts.week_sheet)) {
|
|
47
51
|
return disabledPunchControls();
|
|
48
52
|
}
|
|
49
53
|
const day = opts.week_sheet?.sheets.find((sheet) => sheet.week_day === opts.week_day);
|
|
@@ -207,7 +211,13 @@ export class TimesheetsFX {
|
|
|
207
211
|
});
|
|
208
212
|
}
|
|
209
213
|
const ids = [];
|
|
210
|
-
for (const
|
|
214
|
+
for (const expected of opts.week_sheets) {
|
|
215
|
+
// Query cleaners can return partial days. Always authorize the whole fresh record.
|
|
216
|
+
const week_sheet = await this.findWeekSheet(expected.id);
|
|
217
|
+
if (week_sheet.updated_at !== expected.updated_at)
|
|
218
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
219
|
+
if (!timesheetCanApprove(week_sheet))
|
|
220
|
+
throw new PreActionBlocked(TimesheetsBlock.Locked);
|
|
211
221
|
const id = requiredID(week_sheet.id, "week_sheets", msgWeekSheetIDRequired);
|
|
212
222
|
if (week_sheet.employer_id !== employer_id) {
|
|
213
223
|
throw errValidation({
|
|
@@ -266,6 +276,12 @@ export class TimesheetsFX {
|
|
|
266
276
|
return summary;
|
|
267
277
|
}
|
|
268
278
|
async syncSheets(opts) {
|
|
279
|
+
const source = await this.findWeekSheet(opts.from.id);
|
|
280
|
+
if (source.updated_at !== opts.from.updated_at)
|
|
281
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
282
|
+
if (!timesheetCanApprove(source))
|
|
283
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
284
|
+
opts = { ...opts, from: source };
|
|
269
285
|
const from_id = requiredID(opts.from.id, "from", msgWeekSheetIDRequired);
|
|
270
286
|
if (opts.to.length === 0) {
|
|
271
287
|
throw errRequired({
|
|
@@ -274,7 +290,16 @@ export class TimesheetsFX {
|
|
|
274
290
|
});
|
|
275
291
|
}
|
|
276
292
|
const to_ids = [];
|
|
277
|
-
for (const
|
|
293
|
+
for (const expected of opts.to) {
|
|
294
|
+
const week_sheet = await this.findWeekSheet(expected.id);
|
|
295
|
+
if (week_sheet.updated_at !== expected.updated_at)
|
|
296
|
+
throw new PreActionBlocked(TimesheetsBlock.Changed);
|
|
297
|
+
if (!timesheetIsMutable(week_sheet) ||
|
|
298
|
+
timesheetPunches(week_sheet).some((p) => p.punch_status === PunchStatusWorking) ||
|
|
299
|
+
week_sheet.id === opts.from.id ||
|
|
300
|
+
week_sheet.period_index !== opts.from.period_index ||
|
|
301
|
+
week_sheet.timezone !== opts.from.timezone)
|
|
302
|
+
throw new PreActionBlocked(TimesheetsBlock.InvalidSync);
|
|
278
303
|
const id = requiredID(week_sheet.id, "to", msgWeekSheetIDRequired);
|
|
279
304
|
if (week_sheet.is_archived) {
|
|
280
305
|
throw errValidation({
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BillableOperator, 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?: string[];
|
|
6
|
+
employer_ids?: string[];
|
|
7
|
+
period_indexes?: number[];
|
|
8
|
+
location_ids?: string[];
|
|
9
|
+
is_archived?: boolean;
|
|
10
|
+
punch_status?: string;
|
|
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 timesheetIsMutable(sheet: WeekSheet): boolean;
|
|
51
|
+
export declare function timesheetCanApprove(sheet: WeekSheet): boolean;
|
|
52
|
+
export declare function timesheetStateKey(state: TimesheetPunchState): string;
|
|
53
|
+
export declare function timesheetWeekDay(date: InDate): number;
|
|
54
|
+
export declare class TimesheetsWorkspace {
|
|
55
|
+
private readonly client;
|
|
56
|
+
private readonly actions;
|
|
57
|
+
private readonly billable?;
|
|
58
|
+
private readonly now;
|
|
59
|
+
constructor(client: TimesheetsOperator, actions: TimesheetsFXOperator, billable?: BillableOperator | undefined, now?: () => Date);
|
|
60
|
+
weeks(opts: TimesheetFilters): Promise<WeekSheet[]>;
|
|
61
|
+
summaries(opts: TimesheetFilters): Promise<Summary[]>;
|
|
62
|
+
week(id: string, signal?: AbortSignal): Promise<WeekSheet>;
|
|
63
|
+
summary(id: string, signal?: AbortSignal): Promise<Summary>;
|
|
64
|
+
periods(employer_id: string, start: InDate, end?: InDate): Promise<number[]>;
|
|
65
|
+
punchState(opts: PunchStateOptions): Promise<TimesheetPunchState>;
|
|
66
|
+
punch(opts: CheckedPunchOptions): Promise<WeekSheet>;
|
|
67
|
+
saveDay(opts: SaveTimesheetDayOptions): Promise<WeekSheet>;
|
|
68
|
+
removeDay(owner_id: string, expected: WeekSheet, date: InDate): Promise<void>;
|
|
69
|
+
removeWeek(actor_id: string, expected: WeekSheet): Promise<void>;
|
|
70
|
+
blankWeek(opts: PunchStateOptions & {
|
|
71
|
+
location_id: string;
|
|
72
|
+
date: InDate;
|
|
73
|
+
}): Promise<WeekSheet>;
|
|
74
|
+
private checkedWeek;
|
|
75
|
+
private validateDay;
|
|
76
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { PunchStatusApproved, PunchStatusPending, PunchStatusWorking, PunchTypeIn, PunchTypeOut, PunchTypeRideIn, PunchTypeRideOut, } from "@fabriktor/schema";
|
|
2
|
+
import { inDateAtTimestamp, zeroDate } from "../core/date.js";
|
|
3
|
+
import { PreActionBlocked } from "../core/pre-action.js";
|
|
4
|
+
export const TimesheetsBlock = {
|
|
5
|
+
Changed: "timesheets_changed",
|
|
6
|
+
Locked: "timesheets_locked",
|
|
7
|
+
Active: "timesheets_active",
|
|
8
|
+
InvalidDay: "timesheets_invalid_day",
|
|
9
|
+
InvalidSync: "timesheets_invalid_sync",
|
|
10
|
+
Unconfirmed: "timesheets_unconfirmed",
|
|
11
|
+
};
|
|
12
|
+
function requireState(allowed, reason) {
|
|
13
|
+
if (!allowed)
|
|
14
|
+
throw new PreActionBlocked(reason);
|
|
15
|
+
}
|
|
16
|
+
export function timesheetPunches(sheet) {
|
|
17
|
+
return sheet.sheets.flatMap((day) => day.punches);
|
|
18
|
+
}
|
|
19
|
+
export function timesheetIsMutable(sheet) {
|
|
20
|
+
return (!sheet.is_archived &&
|
|
21
|
+
!timesheetPunches(sheet).some((p) => p.punch_status === PunchStatusApproved));
|
|
22
|
+
}
|
|
23
|
+
export function timesheetCanApprove(sheet) {
|
|
24
|
+
const punches = timesheetPunches(sheet);
|
|
25
|
+
return (timesheetIsMutable(sheet) &&
|
|
26
|
+
punches.length > 0 &&
|
|
27
|
+
punches.every((p) => p.punch_status === PunchStatusPending));
|
|
28
|
+
}
|
|
29
|
+
export function timesheetStateKey(state) {
|
|
30
|
+
return JSON.stringify([
|
|
31
|
+
state.week_sheet?.id,
|
|
32
|
+
state.week_sheet?.updated_at,
|
|
33
|
+
state.week_sheet?.is_archived,
|
|
34
|
+
state.week_day,
|
|
35
|
+
state.punch,
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
export function timesheetWeekDay(date) {
|
|
39
|
+
// Calendar arithmetic on explicit date parts; never the device timezone.
|
|
40
|
+
return new Date(Date.UTC(date.y, date.m - 1, date.d)).getUTCDay();
|
|
41
|
+
}
|
|
42
|
+
function sameDay(a, b) {
|
|
43
|
+
return a.y === b.y && a.m === b.m && a.d === b.d;
|
|
44
|
+
}
|
|
45
|
+
function wallTime(date) {
|
|
46
|
+
return Date.UTC(date.y, date.m - 1, date.d, date.h, date.min);
|
|
47
|
+
}
|
|
48
|
+
function schemaDate(date) {
|
|
49
|
+
// The backend computes UTC from the supplied wall-clock fields and tz.
|
|
50
|
+
return { ...zeroDate(), ...date };
|
|
51
|
+
}
|
|
52
|
+
export class TimesheetsWorkspace {
|
|
53
|
+
client;
|
|
54
|
+
actions;
|
|
55
|
+
billable;
|
|
56
|
+
now;
|
|
57
|
+
constructor(client, actions, billable, now = () => new Date()) {
|
|
58
|
+
this.client = client;
|
|
59
|
+
this.actions = actions;
|
|
60
|
+
this.billable = billable;
|
|
61
|
+
this.now = now;
|
|
62
|
+
}
|
|
63
|
+
async weeks(opts) {
|
|
64
|
+
if (emptyScope(opts))
|
|
65
|
+
return [];
|
|
66
|
+
const result = await this.client.queryWeekSheets({
|
|
67
|
+
signal: opts.signal,
|
|
68
|
+
query: {
|
|
69
|
+
...query(opts),
|
|
70
|
+
is_archived: opts.is_archived,
|
|
71
|
+
"sheets.punches.location_id": opts.location_ids,
|
|
72
|
+
"sheets.punches.punch_status": opts.punch_status,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return result.value ?? [];
|
|
76
|
+
}
|
|
77
|
+
async summaries(opts) {
|
|
78
|
+
if (emptyScope(opts))
|
|
79
|
+
return [];
|
|
80
|
+
const result = await this.client.querySummaries({
|
|
81
|
+
signal: opts.signal,
|
|
82
|
+
query: {
|
|
83
|
+
...query(opts),
|
|
84
|
+
"location_records.location_id": opts.location_ids,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return result.value ?? [];
|
|
88
|
+
}
|
|
89
|
+
async week(id, signal) {
|
|
90
|
+
const { value } = await this.client.findOneWeekSheet({ id, signal });
|
|
91
|
+
requireState(!!value, TimesheetsBlock.Changed);
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
async summary(id, signal) {
|
|
95
|
+
const { value } = await this.client.findOneSummary({ id, signal });
|
|
96
|
+
requireState(!!value, TimesheetsBlock.Changed);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
async periods(employer_id, start, end = start) {
|
|
100
|
+
requireState(!!this.billable && !!employer_id, TimesheetsBlock.InvalidDay);
|
|
101
|
+
const result = await this.billable.periodsFromDates({
|
|
102
|
+
employer_id,
|
|
103
|
+
start,
|
|
104
|
+
end,
|
|
105
|
+
});
|
|
106
|
+
requireState(!!result.value?.ints.length, TimesheetsBlock.InvalidDay);
|
|
107
|
+
return result.value.ints;
|
|
108
|
+
}
|
|
109
|
+
async punchState(opts) {
|
|
110
|
+
requireState(!!opts.owner_id && !!opts.timezone, TimesheetsBlock.InvalidDay);
|
|
111
|
+
const today = opts.date ?? inDateAtTimestamp(this.now().toISOString(), opts.timezone);
|
|
112
|
+
const week_day = timesheetWeekDay(today);
|
|
113
|
+
// Check every open work punch, including other employers/locations/weeks.
|
|
114
|
+
// A scope selection must never hide an already open shift.
|
|
115
|
+
const active = await this.weeks({
|
|
116
|
+
owner_ids: [opts.owner_id],
|
|
117
|
+
is_archived: false,
|
|
118
|
+
punch_status: PunchStatusWorking,
|
|
119
|
+
});
|
|
120
|
+
const open = active.flatMap((week_sheet) => week_sheet.sheets.flatMap((day) => day.punches
|
|
121
|
+
.filter((p) => p.punch_status === PunchStatusWorking)
|
|
122
|
+
.map((punch) => ({ week_sheet, punch, week_day: day.week_day }))));
|
|
123
|
+
requireState(open.length <= 1, TimesheetsBlock.Active);
|
|
124
|
+
if (open[0]) {
|
|
125
|
+
const current = open[0];
|
|
126
|
+
const date = inDateAtTimestamp(this.now().toISOString(), current.punch.timezone);
|
|
127
|
+
return {
|
|
128
|
+
...current,
|
|
129
|
+
stale_day: !sameDay(current.punch.punch_in_time, opts.date ?? date),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (!opts.employer_id)
|
|
133
|
+
return { week_day, stale_day: false };
|
|
134
|
+
const indexes = await this.periods(opts.employer_id, today);
|
|
135
|
+
const weeks = await this.weeks({
|
|
136
|
+
owner_ids: [opts.owner_id],
|
|
137
|
+
employer_ids: [opts.employer_id],
|
|
138
|
+
period_indexes: indexes,
|
|
139
|
+
});
|
|
140
|
+
requireState(weeks.length <= 1, TimesheetsBlock.Changed);
|
|
141
|
+
const week_sheet = weeks[0];
|
|
142
|
+
const punch = week_sheet?.sheets
|
|
143
|
+
.find((day) => day.week_day === week_day)
|
|
144
|
+
?.punches.at(-1);
|
|
145
|
+
return { week_sheet, punch, week_day, stale_day: false };
|
|
146
|
+
}
|
|
147
|
+
async punch(opts) {
|
|
148
|
+
if (opts.date)
|
|
149
|
+
this.validateDay(opts.date, opts.timezone);
|
|
150
|
+
const current = await this.punchState(opts);
|
|
151
|
+
requireState(timesheetStateKey(current) === timesheetStateKey(opts.expected), TimesheetsBlock.Changed);
|
|
152
|
+
requireState(!current.stale_day, TimesheetsBlock.InvalidDay);
|
|
153
|
+
requireState(!current.week_sheet || timesheetIsMutable(current.week_sheet), TimesheetsBlock.Locked);
|
|
154
|
+
const controls = this.actions.punchControls(current);
|
|
155
|
+
const allowed = {
|
|
156
|
+
[PunchTypeIn]: controls.can_punch_in,
|
|
157
|
+
[PunchTypeOut]: controls.can_punch_out,
|
|
158
|
+
[PunchTypeRideIn]: controls.can_ride_in,
|
|
159
|
+
[PunchTypeRideOut]: controls.can_ride_out,
|
|
160
|
+
};
|
|
161
|
+
requireState(allowed[opts.action], TimesheetsBlock.Changed);
|
|
162
|
+
if (opts.action !== PunchTypeIn) {
|
|
163
|
+
requireState(current.punch?.location_id === opts.location_id &&
|
|
164
|
+
current.punch.employer_id === opts.employer_id &&
|
|
165
|
+
current.punch.timezone === opts.timezone, TimesheetsBlock.Changed);
|
|
166
|
+
}
|
|
167
|
+
if (opts.date && current.punch && opts.action !== PunchTypeIn) {
|
|
168
|
+
requireState(sameDay(opts.date, current.punch.punch_in_time) &&
|
|
169
|
+
wallTime(opts.date) >= wallTime(current.punch.punch_in_time), TimesheetsBlock.InvalidDay);
|
|
170
|
+
}
|
|
171
|
+
const context = {
|
|
172
|
+
employer_id: opts.employer_id,
|
|
173
|
+
location_id: opts.location_id,
|
|
174
|
+
timezone: opts.timezone,
|
|
175
|
+
explicit_date: opts.date ? schemaDate(opts.date) : undefined,
|
|
176
|
+
};
|
|
177
|
+
const result = opts.action === PunchTypeIn
|
|
178
|
+
? await this.actions.punchIn(context)
|
|
179
|
+
: opts.action === PunchTypeOut
|
|
180
|
+
? await this.actions.punchOut({
|
|
181
|
+
...context,
|
|
182
|
+
total_pause_minutes: opts.total_pause_minutes ?? 0,
|
|
183
|
+
total_meal_minutes: opts.total_meal_minutes ?? 0,
|
|
184
|
+
})
|
|
185
|
+
: opts.action === PunchTypeRideIn
|
|
186
|
+
? await this.actions.rideIn(context)
|
|
187
|
+
: await this.actions.rideOut(context);
|
|
188
|
+
requireState(!!result.id, TimesheetsBlock.Unconfirmed);
|
|
189
|
+
const sheet = await this.week(result.id);
|
|
190
|
+
const last = sheet.sheets
|
|
191
|
+
.find((day) => day.week_day === current.week_day)
|
|
192
|
+
?.punches.at(-1);
|
|
193
|
+
requireState(!!last &&
|
|
194
|
+
last.location_id === opts.location_id &&
|
|
195
|
+
!sheet.is_archived &&
|
|
196
|
+
(opts.action === PunchTypeOut
|
|
197
|
+
? last.punch_status === PunchStatusPending
|
|
198
|
+
: last.punch_status === PunchStatusWorking) &&
|
|
199
|
+
(opts.action !== PunchTypeRideIn ||
|
|
200
|
+
last.rides.at(-1)?.is_active === true) &&
|
|
201
|
+
(opts.action !== PunchTypeRideOut ||
|
|
202
|
+
last.rides.at(-1)?.is_active === false), TimesheetsBlock.Unconfirmed);
|
|
203
|
+
return sheet;
|
|
204
|
+
}
|
|
205
|
+
async saveDay(opts) {
|
|
206
|
+
this.validateDay(opts.start, opts.timezone);
|
|
207
|
+
this.validateDay(opts.end, opts.timezone);
|
|
208
|
+
requireState(sameDay(opts.start, opts.end) &&
|
|
209
|
+
wallTime(opts.end) > wallTime(opts.start), TimesheetsBlock.InvalidDay);
|
|
210
|
+
const indexes = await this.periods(opts.employer_id, opts.start);
|
|
211
|
+
const matches = await this.weeks({
|
|
212
|
+
owner_ids: [opts.owner_id],
|
|
213
|
+
employer_ids: [opts.employer_id],
|
|
214
|
+
period_indexes: indexes,
|
|
215
|
+
});
|
|
216
|
+
requireState(matches.length <= 1, TimesheetsBlock.Changed);
|
|
217
|
+
const sheet = matches[0];
|
|
218
|
+
const day = sheet?.sheets.find((d) => d.week_day === timesheetWeekDay(opts.start));
|
|
219
|
+
if (opts.replace) {
|
|
220
|
+
requireState(sheet?.id === opts.replace.id &&
|
|
221
|
+
sheet.updated_at === opts.replace.updated_at, TimesheetsBlock.Changed);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// A new manual entry must not silently replace an existing day.
|
|
225
|
+
requireState(!day?.punches.length, TimesheetsBlock.Changed);
|
|
226
|
+
}
|
|
227
|
+
requireState(!sheet || timesheetIsMutable(sheet), TimesheetsBlock.Locked);
|
|
228
|
+
requireState(!day?.punches.some((p) => p.punch_status === PunchStatusWorking), TimesheetsBlock.Active);
|
|
229
|
+
const open = await this.weeks({
|
|
230
|
+
owner_ids: [opts.owner_id],
|
|
231
|
+
is_archived: false,
|
|
232
|
+
punch_status: PunchStatusWorking,
|
|
233
|
+
});
|
|
234
|
+
requireState(open.length === 0, TimesheetsBlock.Active);
|
|
235
|
+
const result = await this.actions.replaceDay({
|
|
236
|
+
employer_id: opts.employer_id,
|
|
237
|
+
location_id: opts.location_id,
|
|
238
|
+
timezone: opts.timezone,
|
|
239
|
+
punch_in_date: schemaDate(opts.start),
|
|
240
|
+
punch_out_date: schemaDate(opts.end),
|
|
241
|
+
total_pause_minutes: opts.total_pause_minutes,
|
|
242
|
+
total_meal_minutes: opts.total_meal_minutes,
|
|
243
|
+
});
|
|
244
|
+
requireState(!!result.punch_out.id, TimesheetsBlock.Unconfirmed);
|
|
245
|
+
return this.week(result.punch_out.id);
|
|
246
|
+
}
|
|
247
|
+
async removeDay(owner_id, expected, date) {
|
|
248
|
+
const sheet = await this.checkedWeek(expected);
|
|
249
|
+
requireState(sheet.owner_id === owner_id, TimesheetsBlock.Locked);
|
|
250
|
+
const day = sheet.sheets.find((d) => d.week_day === timesheetWeekDay(date));
|
|
251
|
+
requireState(!!day?.punches.length &&
|
|
252
|
+
!day.punches.some((p) => p.punch_status === PunchStatusWorking), TimesheetsBlock.Active);
|
|
253
|
+
const punch = day.punches[0];
|
|
254
|
+
requireState(sameDay(date, punch.punch_in_time), TimesheetsBlock.InvalidDay);
|
|
255
|
+
await this.actions.deleteDay({
|
|
256
|
+
employer_id: sheet.employer_id,
|
|
257
|
+
location_id: punch.location_id,
|
|
258
|
+
timezone: punch.timezone,
|
|
259
|
+
explicit_date: schemaDate(date),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async removeWeek(actor_id, expected) {
|
|
263
|
+
const sheet = await this.checkedWeek(expected);
|
|
264
|
+
requireState(sheet.owner_id === actor_id || sheet.employer_id === actor_id, TimesheetsBlock.Locked);
|
|
265
|
+
requireState(!timesheetPunches(sheet).some((p) => p.punch_status === PunchStatusWorking), TimesheetsBlock.Active);
|
|
266
|
+
await this.client.deleteOneWeekSheet({ id: sheet.id });
|
|
267
|
+
}
|
|
268
|
+
async blankWeek(opts) {
|
|
269
|
+
this.validateDay(opts.date, opts.timezone);
|
|
270
|
+
const indexes = await this.periods(opts.employer_id, opts.date);
|
|
271
|
+
const matches = await this.weeks({
|
|
272
|
+
owner_ids: [opts.owner_id],
|
|
273
|
+
employer_ids: [opts.employer_id],
|
|
274
|
+
period_indexes: indexes,
|
|
275
|
+
});
|
|
276
|
+
requireState(matches.length === 0, TimesheetsBlock.Changed);
|
|
277
|
+
const result = await this.actions.createBlankWeekSheet({
|
|
278
|
+
...opts,
|
|
279
|
+
explicit_date: schemaDate(opts.date),
|
|
280
|
+
});
|
|
281
|
+
requireState(!!result.id, TimesheetsBlock.Unconfirmed);
|
|
282
|
+
return this.week(result.id);
|
|
283
|
+
}
|
|
284
|
+
async checkedWeek(expected) {
|
|
285
|
+
const sheet = await this.week(expected.id);
|
|
286
|
+
requireState(sheet.updated_at === expected.updated_at, TimesheetsBlock.Changed);
|
|
287
|
+
requireState(timesheetIsMutable(sheet), TimesheetsBlock.Locked);
|
|
288
|
+
return sheet;
|
|
289
|
+
}
|
|
290
|
+
validateDay(date, timezone) {
|
|
291
|
+
const now = inDateAtTimestamp(this.now().toISOString(), timezone);
|
|
292
|
+
const calendar = new Date(Date.UTC(date.y, date.m - 1, date.d));
|
|
293
|
+
requireState(!!now &&
|
|
294
|
+
date.tz === timezone &&
|
|
295
|
+
date.y > 1 &&
|
|
296
|
+
date.m >= 1 &&
|
|
297
|
+
date.m <= 12 &&
|
|
298
|
+
calendar.getUTCDate() === date.d &&
|
|
299
|
+
Number.isInteger(date.h) &&
|
|
300
|
+
date.h >= 0 &&
|
|
301
|
+
date.h < 24 &&
|
|
302
|
+
Number.isInteger(date.min) &&
|
|
303
|
+
date.min >= 0 &&
|
|
304
|
+
date.min < 60 &&
|
|
305
|
+
wallTime(date) <= wallTime(now), TimesheetsBlock.InvalidDay);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function emptyScope(opts) {
|
|
309
|
+
return [
|
|
310
|
+
opts.owner_ids,
|
|
311
|
+
opts.employer_ids,
|
|
312
|
+
opts.period_indexes,
|
|
313
|
+
opts.location_ids,
|
|
314
|
+
].some((ids) => ids?.length === 0);
|
|
315
|
+
}
|
|
316
|
+
function query(opts) {
|
|
317
|
+
return {
|
|
318
|
+
owner_id: opts.owner_ids,
|
|
319
|
+
employer_id: opts.employer_ids,
|
|
320
|
+
period_index: opts.period_indexes,
|
|
321
|
+
};
|
|
322
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.115",
|
|
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
|
}
|