@happyvertical/smrt-timesheets 0.55.23 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -0
- package/README.md +156 -11
- package/agents/period-rollup.md +20 -0
- package/dist/attendance/context.d.ts +80 -0
- package/dist/attendance/context.d.ts.map +1 -0
- package/dist/attendance/correction-models.d.ts +39 -0
- package/dist/attendance/correction-models.d.ts.map +1 -0
- package/dist/attendance/correction-service.d.ts +118 -0
- package/dist/attendance/correction-service.d.ts.map +1 -0
- package/dist/attendance/models.d.ts +57 -0
- package/dist/attendance/models.d.ts.map +1 -1
- package/dist/attendance/segments.d.ts +20 -0
- package/dist/attendance/segments.d.ts.map +1 -0
- package/dist/attendance/service.d.ts +106 -22
- package/dist/attendance/service.d.ts.map +1 -1
- package/dist/attendance/write.d.ts +12 -0
- package/dist/attendance/write.d.ts.map +1 -0
- package/dist/attendance.d.ts +2 -0
- package/dist/attendance.d.ts.map +1 -1
- package/dist/attendance.js +3 -323
- package/dist/chunks/{attendance-BVeStEBv.js → attendance-B1jCbqSb.js} +3 -3
- package/dist/chunks/{attendance-BVeStEBv.js.map → attendance-B1jCbqSb.js.map} +1 -1
- package/dist/chunks/attendance-DGVy-iHI.js +1246 -0
- package/dist/chunks/attendance-DGVy-iHI.js.map +1 -0
- package/dist/chunks/models-0-YD5F_o.js +372 -0
- package/dist/chunks/models-0-YD5F_o.js.map +1 -0
- package/dist/chunks/{rollup-U1IO_CMA.js → rollup-CDqzCxI2.js} +211 -6
- package/dist/chunks/rollup-CDqzCxI2.js.map +1 -0
- package/dist/chunks/service-time-entry-t7P-bkOQ.js +227 -0
- package/dist/chunks/service-time-entry-t7P-bkOQ.js.map +1 -0
- package/dist/index.js +5 -5
- package/dist/manifest.json +2965 -1358
- package/dist/rollup/correction-guard.d.ts +12 -0
- package/dist/rollup/correction-guard.d.ts.map +1 -0
- package/dist/rollup/models.d.ts +31 -1
- package/dist/rollup/models.d.ts.map +1 -1
- package/dist/rollup/service.d.ts +25 -7
- package/dist/rollup/service.d.ts.map +1 -1
- package/dist/rollup/types.d.ts +18 -0
- package/dist/rollup/types.d.ts.map +1 -1
- package/dist/rollup/write.d.ts +2 -0
- package/dist/rollup/write.d.ts.map +1 -1
- package/dist/rollup.d.ts +3 -2
- package/dist/rollup.d.ts.map +1 -1
- package/dist/rollup.js +2 -2
- package/dist/smrt-knowledge.json +700 -34
- package/package.json +7 -7
- package/dist/attendance.js.map +0 -1
- package/dist/chunks/models-zU8Um7c2.js +0 -176
- package/dist/chunks/models-zU8Um7c2.js.map +0 -1
- package/dist/chunks/rollup-U1IO_CMA.js.map +0 -1
- package/dist/chunks/service-time-entry-CBv-FF-W.js +0 -227
- package/dist/chunks/service-time-entry-CBv-FF-W.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segments.d.ts","sourceRoot":"","sources":["../../src/attendance/segments.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,4BAA4B,EAC7B,MAAM,aAAa,CAAC;AASrB,4CAA4C;AAC5C,wBAAsB,WAAW,CAC/B,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAMnC;AAED,6CAA6C;AAC7C,wBAAsB,YAAY,CAChC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAK9B;AAED,sFAAsF;AACtF,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,EACtB,EAAE,EAAE,IAAI,EACR,MAAM,EAAE,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC,GAChD,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CASnC;AAED,wEAAwE;AACxE,wBAAsB,YAAY,CAChC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,EACtB,EAAE,EAAE,IAAI,EACR,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,iBAAiB,CAAC,CA4B5B;AAED,+EAA+E;AAC/E,wBAAsB,cAAc,CAClC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,EACtB,EAAE,EAAE,IAAI,EACR,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAQ5B;AAED,kEAAkE;AAClE,wBAAsB,UAAU,CAC9B,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,EACtB,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,EACtB,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,iBAAiB,CAAC,CAoB5B"}
|
|
@@ -1,27 +1,77 @@
|
|
|
1
1
|
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
-
import {
|
|
2
|
+
import { type AttendanceActor, type AttendanceBreakOptions, AttendanceConflict, type AttendanceContext, type AttendanceDeviceRef, type AttendancePunchOptions, type AttendanceRefusalCode, type AttendanceWork } from './context.js';
|
|
3
|
+
import { AttendancePunch, type AttendanceReplayOutcome, AttendanceSegment } from './models.js';
|
|
4
|
+
export { type AttendanceActor, type AttendanceBreakOptions, AttendanceConflict, type AttendanceContext, type AttendanceDeviceRef, type AttendancePunchOptions, type AttendanceRefusalCode, type AttendanceWork, validateAttendanceWork, } from './context.js';
|
|
3
5
|
export type { AttendanceReplayOutcome } from './models.js';
|
|
4
|
-
|
|
5
|
-
export interface AttendanceActor {
|
|
6
|
-
tenantId: string;
|
|
7
|
-
profileId: string;
|
|
8
|
-
}
|
|
9
|
-
/** Explicit work attribution for a closed punch's draft evidence entry. */
|
|
10
|
-
export interface AttendanceWork {
|
|
11
|
-
workRefType: string;
|
|
12
|
-
workRefId: string;
|
|
13
|
-
description: string;
|
|
14
|
-
}
|
|
6
|
+
export * from './segments.js';
|
|
15
7
|
/** Offline command with a durable client-generated identifier and device timestamp. */
|
|
16
8
|
export interface AttendanceTap {
|
|
17
9
|
clientId: string;
|
|
18
10
|
at: string;
|
|
19
|
-
action:
|
|
11
|
+
action: AttendanceTapAction;
|
|
20
12
|
paid?: boolean;
|
|
13
|
+
/** Required by `startWork` and `switchWork`; also links evidence on `punchOut`. */
|
|
21
14
|
work?: AttendanceWork;
|
|
15
|
+
/** Recorded on the punch by `punchIn`; hashed with the tap. */
|
|
16
|
+
source?: string;
|
|
17
|
+
/** Recorded on the punch by `punchIn`; hashed with the tap. */
|
|
18
|
+
device?: AttendanceDeviceRef;
|
|
19
|
+
/** Recorded on the break by `startBreak`; hashed with the tap. */
|
|
20
|
+
kind?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Punch and break actions plus the work-segment actions (`stopWork` is "pause"). */
|
|
23
|
+
export type AttendanceTapAction = 'punchIn' | 'punchOut' | 'startBreak' | 'endBreak' | 'startWork' | 'switchWork' | 'stopWork' | 'resumeWork';
|
|
24
|
+
/** What a time policy sees for one tap that has not been replayed yet. */
|
|
25
|
+
export interface AttendanceTimeInput {
|
|
26
|
+
/** The tap exactly as sent. */
|
|
27
|
+
tap: AttendanceTap;
|
|
28
|
+
/** The tap's device time. */
|
|
29
|
+
deviceAt: Date;
|
|
30
|
+
/** When the server received the batch. */
|
|
31
|
+
serverNow: Date;
|
|
32
|
+
/** `serverNow - deviceAt` in milliseconds: how long the tap waited. */
|
|
33
|
+
waitedMs: number;
|
|
34
|
+
/** The trusted actor the tap replays for. */
|
|
35
|
+
actor: AttendanceActor;
|
|
36
|
+
}
|
|
37
|
+
/** The effective time a policy chose, and whether a person should look at it. */
|
|
38
|
+
export interface AttendanceResolvedTime {
|
|
39
|
+
at: Date;
|
|
40
|
+
/** Flags the resulting work segment (punches are always flagged offline). */
|
|
41
|
+
reviewRequired?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Why a consumer refuses or rewrites the stored reason of a refused tap. */
|
|
44
|
+
export interface AttendanceRefusal {
|
|
45
|
+
code: AttendanceRefusalCode;
|
|
46
|
+
/** Replaces the default message when given. */
|
|
47
|
+
message?: string;
|
|
22
48
|
}
|
|
23
|
-
/**
|
|
24
|
-
export
|
|
49
|
+
/** A resolved auto-close deadline and, optionally, why the punch closed then. */
|
|
50
|
+
export interface AttendanceAutoCloseDecision {
|
|
51
|
+
deadline: Date;
|
|
52
|
+
/** Recorded as `autoCloseReason` (1 to 200 characters); defaults to `deadline`. */
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fixed maximum open seconds, or a per-punch resolver returning the deadline
|
|
57
|
+
* (a Date or a decision) or null to leave the punch open.
|
|
58
|
+
*/
|
|
59
|
+
export type AttendanceAutoCloseDeadline = number | ((punch: AttendancePunch) => Date | AttendanceAutoCloseDecision | null | undefined | Promise<Date | AttendanceAutoCloseDecision | null | undefined>);
|
|
60
|
+
export interface AttendanceReplayOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Choose each tap's effective time (for example the device time inside a
|
|
63
|
+
* window, else the server arrival time). The receipt still hashes the tap as
|
|
64
|
+
* sent, so retries with the same clientId match; the resolved time is what is
|
|
65
|
+
* stored. Called once per tap that has no receipt yet, inside its transaction.
|
|
66
|
+
*/
|
|
67
|
+
resolveAt?: (input: AttendanceTimeInput) => AttendanceResolvedTime | Promise<AttendanceResolvedTime>;
|
|
68
|
+
/** Clock for `serverNow`; defaults to `new Date()` per tap. */
|
|
69
|
+
now?: () => Date;
|
|
70
|
+
/**
|
|
71
|
+
* Name the stored code (and optionally message) of a refused tap. Without it,
|
|
72
|
+
* the refusal's own `AttendanceConflict.code` is stored.
|
|
73
|
+
*/
|
|
74
|
+
refusal?: (tap: AttendanceTap, error: AttendanceConflict) => AttendanceRefusal | undefined | void;
|
|
25
75
|
}
|
|
26
76
|
/**
|
|
27
77
|
* Attendance mutations bound to a trusted tenant/profile. Applications authorize
|
|
@@ -34,31 +84,65 @@ export declare class AttendanceService {
|
|
|
34
84
|
private readonly actor;
|
|
35
85
|
constructor(db: DatabaseInterface, actor: AttendanceActor);
|
|
36
86
|
private identity;
|
|
87
|
+
/**
|
|
88
|
+
* Run `operation` in the actor's serialized attendance transaction. The
|
|
89
|
+
* context's collections share the executor, so corrections and replay can
|
|
90
|
+
* compose several attendance mutations (with the segment helpers) atomically.
|
|
91
|
+
* Results that are punches or segments are re-read after commit.
|
|
92
|
+
*/
|
|
93
|
+
withTransaction<T>(operation: (context: AttendanceContext) => Promise<T>): Promise<T>;
|
|
37
94
|
private transact;
|
|
38
95
|
private date;
|
|
39
96
|
private open;
|
|
40
97
|
private chronological;
|
|
41
98
|
/** Open one punch; the database unique slot also protects direct model writers. */
|
|
42
|
-
punchIn(at?: Date): Promise<AttendancePunch>;
|
|
99
|
+
punchIn(at?: Date, options?: AttendancePunchOptions): Promise<AttendancePunch>;
|
|
43
100
|
/** Close the punch and any active break; optionally create linked draft evidence. */
|
|
44
101
|
punchOut(at: Date, work?: AttendanceWork): Promise<AttendancePunch>;
|
|
45
102
|
/** Begin a break; paid defaults false and must be explicitly configured by the caller. */
|
|
46
|
-
startBreak(at: Date, paid?: boolean): Promise<AttendancePunch>;
|
|
47
|
-
/**
|
|
48
|
-
|
|
103
|
+
startBreak(at: Date, paid?: boolean, options?: AttendanceBreakOptions): Promise<AttendancePunch>;
|
|
104
|
+
/**
|
|
105
|
+
* End the current break. With `{ resumeWork: true }` the work interrupted by
|
|
106
|
+
* `startBreak` resumes at the same instant, atomically with the break end.
|
|
107
|
+
*/
|
|
108
|
+
endBreak(at: Date, options?: {
|
|
109
|
+
resumeWork?: boolean;
|
|
110
|
+
}): Promise<AttendancePunch>;
|
|
111
|
+
/** Open a work segment in the open punch; refuses during a break or while one is open. */
|
|
112
|
+
startWork(at: Date, work: AttendanceWork): Promise<AttendanceSegment>;
|
|
113
|
+
/** Atomically close the open segment and open the next one at `at`. */
|
|
114
|
+
switchWork(at: Date, work: AttendanceWork): Promise<AttendanceSegment>;
|
|
115
|
+
/** Close the open segment without starting another. */
|
|
116
|
+
stopWork(at: Date): Promise<AttendanceSegment>;
|
|
117
|
+
/** Reopen the work a break interrupted (the latest segment must have closed on `startBreak`). */
|
|
118
|
+
resumeWork(at: Date): Promise<AttendanceSegment>;
|
|
119
|
+
/** The open work segment of the actor's open punch, or null. */
|
|
120
|
+
currentWork(): Promise<AttendanceSegment | null>;
|
|
121
|
+
/** Work segments of one of the actor's punches, oldest first. */
|
|
122
|
+
segments(punchId: string): Promise<AttendanceSegment[]>;
|
|
49
123
|
private transition;
|
|
124
|
+
/** Run one replayed tap on `tx`; work actions share the punch changes' transaction. */
|
|
125
|
+
private apply;
|
|
50
126
|
private validateWork;
|
|
51
127
|
private link;
|
|
52
128
|
/** Idempotently link a closed, owned punch to draft service evidence. */
|
|
53
129
|
linkServiceTimeEntry(punchId: string, work: AttendanceWork): Promise<AttendancePunch>;
|
|
54
|
-
/**
|
|
55
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Close the open punch once its deadline has passed, flagging it for review.
|
|
132
|
+
* `deadline` is either a fixed maximum open duration in seconds (closing at
|
|
133
|
+
* `startedAt + seconds`, reason `maxDuration`) or a resolver called with the
|
|
134
|
+
* open punch that returns its deadline (a `Date`, or `{ deadline, reason }`),
|
|
135
|
+
* or null to leave it open. The consumer owns time zones and shop-day rules.
|
|
136
|
+
* The open work segment and break close at the deadline; returns the closed
|
|
137
|
+
* punch, or null when nothing was due.
|
|
138
|
+
*/
|
|
139
|
+
autoClose(now: Date, deadline: AttendanceAutoCloseDeadline): Promise<AttendancePunch | null>;
|
|
56
140
|
/**
|
|
57
141
|
* Replay device timestamps in order (ties use clientId). Committed attendance
|
|
58
142
|
* wins: stale/state-conflicting taps get durable refusals; identical retries
|
|
59
143
|
* return the same outcome, changed payloads using a key are rejected. A
|
|
60
144
|
* database failure rolls back its tap and receipt and remains retryable.
|
|
61
145
|
*/
|
|
62
|
-
replay(taps: readonly AttendanceTap[]): Promise<AttendanceReplayOutcome[]>;
|
|
146
|
+
replay(taps: readonly AttendanceTap[], options?: AttendanceReplayOptions): Promise<AttendanceReplayOutcome[]>;
|
|
63
147
|
}
|
|
64
148
|
//# sourceMappingURL=service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/attendance/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/attendance/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SmrtObject } from '@happyvertical/smrt-core';
|
|
2
|
+
/** True while `object` is being saved by `AttendanceCorrectionService`. */
|
|
3
|
+
export declare function isCorrectionWrite(object: SmrtObject): boolean;
|
|
4
|
+
/** Save `object` with closed-interval immutability lifted for this one write. */
|
|
5
|
+
export declare function persistCorrection<T extends SmrtObject>(object: T): Promise<T>;
|
|
6
|
+
/** Delete a closed break or segment through the correction capability. */
|
|
7
|
+
export declare function deleteCorrection<T extends SmrtObject>(object: T): Promise<void>;
|
|
8
|
+
/** True while `AttendanceService.autoClose` is saving `object`. */
|
|
9
|
+
export declare function isAutoCloseWrite(object: SmrtObject): boolean;
|
|
10
|
+
/** Save `object` with its auto-close reason writable for this one write. */
|
|
11
|
+
export declare function persistAutoClose<T extends SmrtObject>(object: T): Promise<T>;
|
|
12
|
+
//# sourceMappingURL=write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/attendance/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAO3D,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAE7D;AAED,iFAAiF;AACjF,wBAAsB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EAC1D,MAAM,EAAE,CAAC,GACR,OAAO,CAAC,CAAC,CAAC,CAQZ;AAED,0EAA0E;AAC1E,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EACzD,MAAM,EAAE,CAAC,GACR,OAAO,CAAC,IAAI,CAAC,CAOf;AAMD,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAE5D;AAED,4EAA4E;AAC5E,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EACzD,MAAM,EAAE,CAAC,GACR,OAAO,CAAC,CAAC,CAAC,CAQZ"}
|
package/dist/attendance.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Tenant-scoped punches, breaks, automatic close and offline replay. */
|
|
2
2
|
import './__smrt-register__.js';
|
|
3
|
+
export * from './attendance/correction-models.js';
|
|
4
|
+
export * from './attendance/correction-service.js';
|
|
3
5
|
export * from './attendance/models.js';
|
|
4
6
|
export * from './attendance/service.js';
|
|
5
7
|
//# sourceMappingURL=attendance.d.ts.map
|
package/dist/attendance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendance.d.ts","sourceRoot":"","sources":["../src/attendance.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,OAAO,wBAAwB,CAAC;AAEhC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"attendance.d.ts","sourceRoot":"","sources":["../src/attendance.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,OAAO,wBAAwB,CAAC;AAEhC,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC"}
|
package/dist/attendance.js
CHANGED
|
@@ -1,323 +1,3 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
import { a as
|
|
3
|
-
|
|
4
|
-
import { getTenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
5
|
-
//#region src/attendance/service.ts
|
|
6
|
-
var AttendanceConflict = class extends Error {};
|
|
7
|
-
var AttendanceService = class {
|
|
8
|
-
constructor(db, actor) {
|
|
9
|
-
this.db = db;
|
|
10
|
-
this.actor = actor;
|
|
11
|
-
if (!actor.tenantId || !actor.profileId) throw new Error("Attendance requires a trusted tenant/profile actor.");
|
|
12
|
-
this.actor = {
|
|
13
|
-
tenantId: this.identity(actor.tenantId),
|
|
14
|
-
profileId: this.identity(actor.profileId)
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
db;
|
|
18
|
-
actor;
|
|
19
|
-
identity(value) {
|
|
20
|
-
if (!isPostgresDatabase(this.db)) return value;
|
|
21
|
-
const hex = value.replace(/[{}-]/g, "").toLowerCase();
|
|
22
|
-
if (!/^[0-9a-f]{32}$/.test(hex)) throw new Error("PostgreSQL attendance identities must be UUIDs.");
|
|
23
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
24
|
-
}
|
|
25
|
-
async transact(operation) {
|
|
26
|
-
const current = getTenantId();
|
|
27
|
-
if (current && this.identity(current) !== this.actor.tenantId) throw new Error("Attendance actor differs from active tenant.");
|
|
28
|
-
return withTenant({ tenantId: this.actor.tenantId }, async () => {
|
|
29
|
-
const result = await withEmbeddedWriteTransaction(this.db, isEmbeddedDatabase(this.db), async (db) => {
|
|
30
|
-
if (isPostgresDatabase(db)) await db.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", JSON.stringify([
|
|
31
|
-
"smrt.attendance",
|
|
32
|
-
this.actor.tenantId,
|
|
33
|
-
this.actor.profileId
|
|
34
|
-
]));
|
|
35
|
-
return operation({
|
|
36
|
-
db,
|
|
37
|
-
punches: await AttendancePunchCollection.create({ db }),
|
|
38
|
-
breaks: await AttendanceBreakCollection.create({ db }),
|
|
39
|
-
replays: await AttendanceReplayCollection.create({ db })
|
|
40
|
-
});
|
|
41
|
-
}, true);
|
|
42
|
-
if (result instanceof AttendancePunch) {
|
|
43
|
-
const [fresh] = await (await AttendancePunchCollection.create({ db: this.db })).list({
|
|
44
|
-
where: {
|
|
45
|
-
...this.actor,
|
|
46
|
-
id: result.id
|
|
47
|
-
},
|
|
48
|
-
limit: 1
|
|
49
|
-
});
|
|
50
|
-
if (!fresh) throw new Error("Committed attendance punch disappeared.");
|
|
51
|
-
return fresh;
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
date(at) {
|
|
57
|
-
if (!(at instanceof Date) || !Number.isFinite(at.getTime())) throw new AttendanceConflict("Attendance timestamps must be valid dates.");
|
|
58
|
-
return at;
|
|
59
|
-
}
|
|
60
|
-
async open(tx) {
|
|
61
|
-
const [punch] = await tx.punches.list({
|
|
62
|
-
where: {
|
|
63
|
-
...this.actor,
|
|
64
|
-
endedAt: null
|
|
65
|
-
},
|
|
66
|
-
limit: 1
|
|
67
|
-
});
|
|
68
|
-
if (!punch) throw new AttendanceConflict("No open punch.");
|
|
69
|
-
return punch;
|
|
70
|
-
}
|
|
71
|
-
async chronological(tx, at) {
|
|
72
|
-
const open = await tx.punches.list({
|
|
73
|
-
where: {
|
|
74
|
-
...this.actor,
|
|
75
|
-
endedAt: null
|
|
76
|
-
},
|
|
77
|
-
limit: 1
|
|
78
|
-
});
|
|
79
|
-
const rows = open.length ? open : await tx.punches.list({
|
|
80
|
-
where: { ...this.actor },
|
|
81
|
-
orderBy: "ended_at DESC",
|
|
82
|
-
limit: 1
|
|
83
|
-
});
|
|
84
|
-
if (!rows.length) return;
|
|
85
|
-
const punch = rows[0];
|
|
86
|
-
const breaks = await tx.breaks.list({ where: {
|
|
87
|
-
tenantId: this.actor.tenantId,
|
|
88
|
-
punchId: punch.id
|
|
89
|
-
} });
|
|
90
|
-
const latest = Math.max(punch.startedAt.getTime(), punch.endedAt?.getTime() ?? 0, ...breaks.flatMap((b) => [b.startedAt.getTime(), b.endedAt?.getTime() ?? 0]));
|
|
91
|
-
if (at.getTime() < latest) throw new AttendanceConflict("Tap predates committed attendance; manual review required.");
|
|
92
|
-
}
|
|
93
|
-
/** Open one punch; the database unique slot also protects direct model writers. */
|
|
94
|
-
async punchIn(at = /* @__PURE__ */ new Date()) {
|
|
95
|
-
return this.transact((tx) => this.transition(tx, "punchIn", this.date(at)));
|
|
96
|
-
}
|
|
97
|
-
/** Close the punch and any active break; optionally create linked draft evidence. */
|
|
98
|
-
async punchOut(at, work) {
|
|
99
|
-
return this.transact((tx) => this.transition(tx, "punchOut", this.date(at), false, work));
|
|
100
|
-
}
|
|
101
|
-
/** Begin a break; paid defaults false and must be explicitly configured by the caller. */
|
|
102
|
-
async startBreak(at, paid = false) {
|
|
103
|
-
return this.transact((tx) => this.transition(tx, "startBreak", this.date(at), paid));
|
|
104
|
-
}
|
|
105
|
-
/** End the current break. */
|
|
106
|
-
async endBreak(at) {
|
|
107
|
-
return this.transact((tx) => this.transition(tx, "endBreak", this.date(at)));
|
|
108
|
-
}
|
|
109
|
-
async transition(tx, action, at, paid = false, work) {
|
|
110
|
-
await this.chronological(tx, at);
|
|
111
|
-
if (action === "punchIn") {
|
|
112
|
-
if ((await tx.punches.list({
|
|
113
|
-
where: {
|
|
114
|
-
...this.actor,
|
|
115
|
-
endedAt: null
|
|
116
|
-
},
|
|
117
|
-
limit: 1
|
|
118
|
-
})).length) throw new AttendanceConflict("A punch is already open.");
|
|
119
|
-
return tx.punches.create({
|
|
120
|
-
...this.actor,
|
|
121
|
-
startedAt: at,
|
|
122
|
-
_insertOnly: true
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
const punch = await this.open(tx);
|
|
126
|
-
const breaks = await tx.breaks.list({ where: {
|
|
127
|
-
tenantId: this.actor.tenantId,
|
|
128
|
-
punchId: punch.id
|
|
129
|
-
} });
|
|
130
|
-
const active = breaks.find((b) => !b.endedAt);
|
|
131
|
-
if (action === "startBreak") {
|
|
132
|
-
if (active) throw new AttendanceConflict("A break is already open.");
|
|
133
|
-
if (typeof paid !== "boolean") throw new AttendanceConflict("paid must be boolean.");
|
|
134
|
-
await tx.breaks.create({
|
|
135
|
-
tenantId: this.actor.tenantId,
|
|
136
|
-
punchId: punch.id ?? void 0,
|
|
137
|
-
startedAt: at,
|
|
138
|
-
paid,
|
|
139
|
-
_insertOnly: true
|
|
140
|
-
});
|
|
141
|
-
return punch;
|
|
142
|
-
}
|
|
143
|
-
if (action !== "endBreak" && action !== "punchOut") throw new AttendanceConflict("Unknown attendance action.");
|
|
144
|
-
if (action === "endBreak" && !active) throw new AttendanceConflict("No open break.");
|
|
145
|
-
if (action === "punchOut" && work) {
|
|
146
|
-
this.validateWork(work);
|
|
147
|
-
const unpaidMilliseconds2 = breaks.filter((b) => !b.paid).reduce((sum, b) => sum + (b.endedAt ?? at).getTime() - b.startedAt.getTime(), 0);
|
|
148
|
-
if (Math.round((at.getTime() - punch.startedAt.getTime() - unpaidMilliseconds2) / 1e3) <= 0) throw new AttendanceConflict("Only closed punches with positive duration can produce service evidence.");
|
|
149
|
-
}
|
|
150
|
-
if (active) {
|
|
151
|
-
active.endedAt = at;
|
|
152
|
-
await active.save();
|
|
153
|
-
}
|
|
154
|
-
if (action === "endBreak") return punch;
|
|
155
|
-
punch.endedAt = at;
|
|
156
|
-
const unpaidMilliseconds = breaks.filter((b) => !b.paid).reduce((sum, b) => sum + b.endedAt.getTime() - b.startedAt.getTime(), 0);
|
|
157
|
-
punch.unpaidBreakSeconds = Math.round(unpaidMilliseconds / 1e3);
|
|
158
|
-
punch.durationSeconds = Math.round((at.getTime() - punch.startedAt.getTime() - unpaidMilliseconds) / 1e3);
|
|
159
|
-
await punch.save();
|
|
160
|
-
if (work) await this.link(tx, punch, work);
|
|
161
|
-
return punch;
|
|
162
|
-
}
|
|
163
|
-
validateWork(work) {
|
|
164
|
-
if (!work || typeof work.workRefType !== "string" || !work.workRefType || typeof work.workRefId !== "string" || !work.workRefId || typeof work.description !== "string") throw new AttendanceConflict("A complete work reference and description are required.");
|
|
165
|
-
}
|
|
166
|
-
async link(tx, punch, work) {
|
|
167
|
-
this.validateWork(work);
|
|
168
|
-
if (!punch.endedAt || punch.durationSeconds <= 0) throw new AttendanceConflict("Only closed punches with positive duration can produce service evidence.");
|
|
169
|
-
const entries = await ServiceTimeEntryCollection.create({ db: tx.db });
|
|
170
|
-
if (punch.serviceTimeEntryId) {
|
|
171
|
-
const [existing] = await entries.list({
|
|
172
|
-
where: {
|
|
173
|
-
id: punch.serviceTimeEntryId,
|
|
174
|
-
tenantId: this.actor.tenantId
|
|
175
|
-
},
|
|
176
|
-
limit: 1
|
|
177
|
-
});
|
|
178
|
-
if (!existing || existing.workRefType !== work.workRefType || existing.workRefId !== work.workRefId || existing.description !== work.description) throw new AttendanceConflict("Punch already linked to different work.");
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const entry = await entries.create({
|
|
182
|
-
tenantId: this.actor.tenantId,
|
|
183
|
-
participantKind: "human",
|
|
184
|
-
participantProfileId: this.actor.profileId,
|
|
185
|
-
workRefType: work.workRefType,
|
|
186
|
-
workRefId: work.workRefId,
|
|
187
|
-
description: work.description,
|
|
188
|
-
source: "timer",
|
|
189
|
-
startedAt: punch.startedAt,
|
|
190
|
-
endedAt: punch.endedAt,
|
|
191
|
-
durationSeconds: punch.durationSeconds,
|
|
192
|
-
status: "draft",
|
|
193
|
-
metadata: JSON.stringify({ attendancePunchId: punch.id }),
|
|
194
|
-
_insertOnly: true
|
|
195
|
-
});
|
|
196
|
-
if (!entry.id) throw new Error("Saved service entry has no id.");
|
|
197
|
-
punch.serviceTimeEntryId = entry.id;
|
|
198
|
-
await punch.save();
|
|
199
|
-
}
|
|
200
|
-
/** Idempotently link a closed, owned punch to draft service evidence. */
|
|
201
|
-
async linkServiceTimeEntry(punchId, work) {
|
|
202
|
-
return this.transact(async (tx) => {
|
|
203
|
-
const [punch] = await tx.punches.list({
|
|
204
|
-
where: {
|
|
205
|
-
...this.actor,
|
|
206
|
-
id: punchId
|
|
207
|
-
},
|
|
208
|
-
limit: 1
|
|
209
|
-
});
|
|
210
|
-
if (!punch) throw new AttendanceConflict("Punch not found for actor.");
|
|
211
|
-
await this.link(tx, punch, work);
|
|
212
|
-
return punch;
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
/** Close at startedAt + maxOpenSeconds once due, flagging the punch for review. */
|
|
216
|
-
async autoClose(now, maxOpenSeconds) {
|
|
217
|
-
this.date(now);
|
|
218
|
-
if (!Number.isSafeInteger(maxOpenSeconds) || maxOpenSeconds <= 0) throw new AttendanceConflict("maxOpenSeconds must be a positive integer.");
|
|
219
|
-
return this.transact(async (tx) => {
|
|
220
|
-
const [punch] = await tx.punches.list({
|
|
221
|
-
where: {
|
|
222
|
-
...this.actor,
|
|
223
|
-
endedAt: null
|
|
224
|
-
},
|
|
225
|
-
limit: 1
|
|
226
|
-
});
|
|
227
|
-
if (!punch) return null;
|
|
228
|
-
const deadline = new Date(punch.startedAt.getTime() + maxOpenSeconds * 1e3);
|
|
229
|
-
if (deadline > now) return null;
|
|
230
|
-
const closed = await this.transition(tx, "punchOut", deadline);
|
|
231
|
-
closed.reviewRequired = true;
|
|
232
|
-
await closed.save();
|
|
233
|
-
return closed;
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Replay device timestamps in order (ties use clientId). Committed attendance
|
|
238
|
-
* wins: stale/state-conflicting taps get durable refusals; identical retries
|
|
239
|
-
* return the same outcome, changed payloads using a key are rejected. A
|
|
240
|
-
* database failure rolls back its tap and receipt and remains retryable.
|
|
241
|
-
*/
|
|
242
|
-
async replay(taps) {
|
|
243
|
-
for (const tap of taps) {
|
|
244
|
-
if (!tap || typeof tap.at !== "string" || typeof tap.clientId !== "string" || !tap.clientId || ![
|
|
245
|
-
"punchIn",
|
|
246
|
-
"punchOut",
|
|
247
|
-
"startBreak",
|
|
248
|
-
"endBreak"
|
|
249
|
-
].includes(tap.action)) throw new AttendanceConflict("Malformed attendance tap.");
|
|
250
|
-
this.date(new Date(tap.at));
|
|
251
|
-
if (tap.work !== void 0) this.validateWork(tap.work);
|
|
252
|
-
if (tap.paid !== void 0 && typeof tap.paid !== "boolean") throw new AttendanceConflict("paid must be boolean.");
|
|
253
|
-
}
|
|
254
|
-
const ordered = [...taps].sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || (a.clientId < b.clientId ? -1 : a.clientId > b.clientId ? 1 : 0));
|
|
255
|
-
const outcomes = [];
|
|
256
|
-
for (const tap of ordered) outcomes.push(await this.transact(async (tx) => {
|
|
257
|
-
const replayKey = JSON.stringify([
|
|
258
|
-
this.actor.tenantId,
|
|
259
|
-
this.actor.profileId,
|
|
260
|
-
tap.clientId
|
|
261
|
-
]);
|
|
262
|
-
const content = JSON.stringify([
|
|
263
|
-
tap.action,
|
|
264
|
-
new Date(tap.at).toISOString(),
|
|
265
|
-
tap.paid ?? false,
|
|
266
|
-
tap.work?.workRefType ?? null,
|
|
267
|
-
tap.work?.workRefId ?? null,
|
|
268
|
-
tap.work?.description ?? null
|
|
269
|
-
]);
|
|
270
|
-
const [receipt] = await tx.replays.list({
|
|
271
|
-
where: {
|
|
272
|
-
tenantId: this.actor.tenantId,
|
|
273
|
-
replayKey
|
|
274
|
-
},
|
|
275
|
-
limit: 1
|
|
276
|
-
});
|
|
277
|
-
if (receipt) {
|
|
278
|
-
if (receipt.content !== content) throw new AttendanceConflict("Client id reused with different content.");
|
|
279
|
-
const outcome2 = receipt.getOutcome();
|
|
280
|
-
if (outcome2.clientId !== tap.clientId) throw new Error("Stored attendance receipt belongs to another client id.");
|
|
281
|
-
return outcome2;
|
|
282
|
-
}
|
|
283
|
-
let outcome;
|
|
284
|
-
try {
|
|
285
|
-
outcome = await withEmbeddedWriteTransaction(tx.db, isEmbeddedDatabase(tx.db), async (db) => {
|
|
286
|
-
const inner = {
|
|
287
|
-
db,
|
|
288
|
-
punches: await AttendancePunchCollection.create({ db }),
|
|
289
|
-
breaks: await AttendanceBreakCollection.create({ db }),
|
|
290
|
-
replays: tx.replays
|
|
291
|
-
};
|
|
292
|
-
const punch = await this.transition(inner, tap.action, new Date(tap.at), tap.paid, tap.work);
|
|
293
|
-
punch.recordedOffline = true;
|
|
294
|
-
punch.reviewRequired = true;
|
|
295
|
-
await punch.save();
|
|
296
|
-
return {
|
|
297
|
-
clientId: tap.clientId,
|
|
298
|
-
punchId: punch.id
|
|
299
|
-
};
|
|
300
|
-
}, true);
|
|
301
|
-
} catch (error) {
|
|
302
|
-
if (!(error instanceof AttendanceConflict)) throw error;
|
|
303
|
-
outcome = {
|
|
304
|
-
clientId: tap.clientId,
|
|
305
|
-
error: error.message
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
await tx.replays.create({
|
|
309
|
-
tenantId: this.actor.tenantId,
|
|
310
|
-
replayKey,
|
|
311
|
-
content,
|
|
312
|
-
outcome: JSON.stringify(outcome),
|
|
313
|
-
_insertOnly: true
|
|
314
|
-
});
|
|
315
|
-
return outcome;
|
|
316
|
-
}));
|
|
317
|
-
return outcomes;
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
//#endregion
|
|
321
|
-
export { AttendanceBreak, AttendanceBreakCollection, AttendanceConflict, AttendancePunch, AttendancePunchCollection, AttendanceReplay, AttendanceReplayCollection, AttendanceService };
|
|
322
|
-
|
|
323
|
-
//# sourceMappingURL=attendance.js.map
|
|
1
|
+
import { a as AttendanceReplay, c as AttendanceSegmentCollection, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, s as AttendanceSegment, t as AttendanceBreak } from "./chunks/models-0-YD5F_o.js";
|
|
2
|
+
import { a as continueSegment, c as openSegment, d as validateAttendanceWork, f as ATTENDANCE_CORRECTION_KINDS, i as closeOpenSegment, l as replaceSegment, m as AttendanceCorrectionCollection, n as AttendanceService, o as endSegment, p as AttendanceCorrection, r as beginSegment, s as listSegments, t as AttendanceCorrectionService, u as AttendanceConflict } from "./chunks/attendance-DGVy-iHI.js";
|
|
3
|
+
export { ATTENDANCE_CORRECTION_KINDS, AttendanceBreak, AttendanceBreakCollection, AttendanceConflict, AttendanceCorrection, AttendanceCorrectionCollection, AttendanceCorrectionService, AttendancePunch, AttendancePunchCollection, AttendanceReplay, AttendanceReplayCollection, AttendanceSegment, AttendanceSegmentCollection, AttendanceService, beginSegment, closeOpenSegment, continueSegment, endSegment, listSegments, openSegment, replaceSegment, validateAttendanceWork };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as AttendancePunchCollection, n as AttendanceBreakCollection } from "./models-
|
|
2
|
-
import { i as integer, n as clipSource, r as instant } from "./rollup-
|
|
1
|
+
import { i as AttendancePunchCollection, n as AttendanceBreakCollection } from "./models-0-YD5F_o.js";
|
|
2
|
+
import { i as integer, n as clipSource, r as instant } from "./rollup-CDqzCxI2.js";
|
|
3
3
|
//#region src/rollup/attendance.ts
|
|
4
4
|
async function attendanceSources(db, actor, period) {
|
|
5
5
|
const punches = await AttendancePunchCollection.create({ db });
|
|
@@ -57,4 +57,4 @@ async function attendanceSources(db, actor, period) {
|
|
|
57
57
|
//#endregion
|
|
58
58
|
export { attendanceSources };
|
|
59
59
|
|
|
60
|
-
//# sourceMappingURL=attendance-
|
|
60
|
+
//# sourceMappingURL=attendance-B1jCbqSb.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendance-
|
|
1
|
+
{"version":3,"file":"attendance-B1jCbqSb.js","names":[],"sources":["../../src/rollup/attendance.ts"],"sourcesContent":["import type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n AttendanceBreakCollection,\n AttendancePunchCollection,\n} from '../attendance/models.js';\nimport { clipSource, instant, integer } from './calculation.js';\nimport type { TimecardActor, TimecardPeriod, TimecardSource } from './types.js';\n\n/** Optional attendance reader. Called on the rollup's transaction executor. */\nexport async function attendanceSources(\n db: DatabaseInterface,\n actor: TimecardActor,\n period: TimecardPeriod,\n): Promise<{ sources: TimecardSource[]; linkedEntryIds: Set<string> }> {\n const punches = await AttendancePunchCollection.create({ db });\n const breaks = await AttendanceBreakCollection.create({ db });\n const sources: TimecardSource[] = [];\n const linkedEntryIds = new Set<string>();\n for (const punch of await punches.list({ where: { ...actor } })) {\n // A linked punch is authoritative even when pending review: never count its\n // evidence entry behind the review gate, or outside the punch's period.\n if (punch.serviceTimeEntryId) linkedEntryIds.add(punch.serviceTimeEntryId);\n if (!punch.endedAt || punch.reviewRequired) continue;\n const start = instant(punch.startedAt);\n const end = instant(punch.endedAt);\n if (end < start) throw new Error('Invalid attendance interval.');\n let cursor = start;\n let unpaidMs = 0;\n const spans: [number, number][] = [];\n for (const pause of await breaks.list({\n where: { tenantId: actor.tenantId, punchId: punch.id },\n orderBy: 'started_at ASC',\n })) {\n if (!pause.endedAt)\n throw new Error('Closed attendance has an open break.');\n const left = instant(pause.startedAt);\n const right = instant(pause.endedAt);\n if (left < cursor || right < left || right > end)\n throw new Error('Invalid or overlapping attendance breaks.');\n if (!pause.paid) {\n spans.push([cursor, left]);\n unpaidMs += right - left;\n } else {\n spans.push([cursor, right]);\n }\n cursor = right;\n }\n spans.push([cursor, end]);\n const net = Math.round((end - start - unpaidMs) / 1000);\n if (integer(punch.durationSeconds) !== net)\n throw new Error('Attendance duration does not match its breaks.');\n // Round elapsed paid prefixes once, preserving conservation across breaks.\n let paidMs = 0;\n for (const [left, right] of spans) {\n const seconds =\n Math.round((paidMs + right - left) / 1000) - Math.round(paidMs / 1000);\n paidMs += right - left;\n const source = clipSource(\n {\n kind: 'attendance',\n id: String(punch.id),\n startsAt: new Date(left).toISOString(),\n endsAt: new Date(right).toISOString(),\n seconds,\n },\n period,\n );\n if (source?.seconds) sources.push(source);\n }\n }\n return { sources, linkedEntryIds };\n}\n"],"mappings":";;;AASA,eAAsB,kBACpB,IACA,OACA,QACqE;CACrE,MAAM,UAAU,MAAM,0BAA0B,OAAO,EAAE,GAAG,CAAC;CAC7D,MAAM,SAAS,MAAM,0BAA0B,OAAO,EAAE,GAAG,CAAC;CAC5D,MAAM,UAA4B,CAAC;CACnC,MAAM,iCAAiB,IAAI,IAAY;CACvC,KAAA,MAAW,SAAS,MAAM,QAAQ,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG;EAG/D,IAAI,MAAM,oBAAoB,eAAe,IAAI,MAAM,kBAAkB;EACzE,IAAI,CAAC,MAAM,WAAW,MAAM,gBAAgB;EAC5C,MAAM,QAAQ,QAAQ,MAAM,SAAS;EACrC,MAAM,MAAM,QAAQ,MAAM,OAAO;EACjC,IAAI,MAAM,OAAO,MAAM,IAAI,MAAM,8BAA8B;EAC/D,IAAI,SAAS;EACb,IAAI,WAAW;EACf,MAAM,QAA4B,CAAC;EACnC,KAAA,MAAW,SAAS,MAAM,OAAO,KAAK;GACpC,OAAO;IAAE,UAAU,MAAM;IAAU,SAAS,MAAM;GAAG;GACrD,SAAS;EACX,CAAC,GAAG;GACF,IAAI,CAAC,MAAM,SACT,MAAM,IAAI,MAAM,sCAAsC;GACxD,MAAM,OAAO,QAAQ,MAAM,SAAS;GACpC,MAAM,QAAQ,QAAQ,MAAM,OAAO;GACnC,IAAI,OAAO,UAAU,QAAQ,QAAQ,QAAQ,KAC3C,MAAM,IAAI,MAAM,2CAA2C;GAC7D,IAAI,CAAC,MAAM,MAAM;IACf,MAAM,KAAK,CAAC,QAAQ,IAAI,CAAC;IACzB,YAAY,QAAQ;GACtB,OACE,MAAM,KAAK,CAAC,QAAQ,KAAK,CAAC;GAE5B,SAAS;EACX;EACA,MAAM,KAAK,CAAC,QAAQ,GAAG,CAAC;EACxB,MAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,YAAY,GAAI;EACtD,IAAI,QAAQ,MAAM,eAAe,MAAM,KACrC,MAAM,IAAI,MAAM,gDAAgD;EAElE,IAAI,SAAS;EACb,KAAA,MAAW,CAAC,MAAM,UAAU,OAAO;GACjC,MAAM,UACJ,KAAK,OAAO,SAAS,QAAQ,QAAQ,GAAI,IAAI,KAAK,MAAM,SAAS,GAAI;GACvE,UAAU,QAAQ;GAClB,MAAM,SAAS,WACb;IACE,MAAM;IACN,IAAI,OAAO,MAAM,EAAE;IACnB,UAAU,IAAI,KAAK,IAAI,CAAA,CAAE,YAAY;IACrC,QAAQ,IAAI,KAAK,KAAK,CAAA,CAAE,YAAY;IACpC;GACF,GACA,MACF;GACA,IAAI,QAAQ,SAAS,QAAQ,KAAK,MAAM;EAC1C;CACF;CACA,OAAO;EAAE;EAAS;CAAe;AACnC"}
|