@happyvertical/smrt-timesheets 0.55.24 → 0.56.1
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 +60 -0
- package/README.md +190 -12
- 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-Cpo4mIID.js → attendance-Cw75uJnT.js} +3 -3
- package/dist/chunks/{attendance-Cpo4mIID.js.map → attendance-Cw75uJnT.js.map} +1 -1
- package/dist/chunks/attendance-DJNytMTN.js +1246 -0
- package/dist/chunks/attendance-DJNytMTN.js.map +1 -0
- package/dist/chunks/models-tUePZ5x7.js +372 -0
- package/dist/chunks/models-tUePZ5x7.js.map +1 -0
- package/dist/chunks/{rollup-Du6uaqyM.js → rollup-CzCb-DxI.js} +211 -6
- package/dist/chunks/rollup-CzCb-DxI.js.map +1 -0
- package/dist/chunks/service-time-entry-BJVSGx33.js +286 -0
- package/dist/chunks/service-time-entry-BJVSGx33.js.map +1 -0
- package/dist/chunks/time-entry-actions-ChT1lcOp.js +576 -0
- package/dist/chunks/time-entry-actions-ChT1lcOp.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -334
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2020 -168
- package/dist/models/service-snapshots.d.ts.map +1 -1
- package/dist/models/service-time-entry.d.ts +36 -1
- package/dist/models/service-time-entry.d.ts.map +1 -1
- package/dist/recipes.d.ts +124 -0
- package/dist/recipes.d.ts.map +1 -0
- 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/services/action-principal.d.ts +54 -0
- package/dist/services/action-principal.d.ts.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/time-entry-actions.d.ts +83 -0
- package/dist/services/time-entry-actions.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +902 -40
- package/package.json +8 -7
- package/dist/attendance.js.map +0 -1
- package/dist/chunks/models-_ZbpXWnb.js +0 -176
- package/dist/chunks/models-_ZbpXWnb.js.map +0 -1
- package/dist/chunks/rollup-Du6uaqyM.js.map +0 -1
- package/dist/chunks/service-time-entry-BGj39_EC.js +0 -227
- package/dist/chunks/service-time-entry-BGj39_EC.js.map +0 -1
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-tUePZ5x7.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-DJNytMTN.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-tUePZ5x7.js";
|
|
2
|
+
import { i as integer, n as clipSource, r as instant } from "./rollup-CzCb-DxI.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-Cw75uJnT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendance-
|
|
1
|
+
{"version":3,"file":"attendance-Cw75uJnT.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"}
|