@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 CHANGED
@@ -108,6 +108,7 @@ export class FX {
108
108
  });
109
109
  this.timesheets = newTimesheetsFX({
110
110
  timesheets: opts.timesheets,
111
+ billable: opts.billable,
111
112
  });
112
113
  this.users = newUsersFX({
113
114
  users: opts.users,
@@ -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?.is_archived) {
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 week_sheet of opts.week_sheets) {
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 week_sheet of opts.to) {
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.113",
3
+ "version": "0.0.115",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
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.85",
20
+ "@fabriktor/client": "0.0.87",
21
21
  "@fabriktor/fx": "0.0.94",
22
22
  "@fabriktor/schema": "0.0.63"
23
23
  }