@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
package/AGENTS.md
CHANGED
|
@@ -15,6 +15,26 @@ Attendance services accept trusted session actors, serialize mutations per
|
|
|
15
15
|
actor, and bind every write to one executor. The new tables use non-null unique
|
|
16
16
|
open slots, not nullable unique keys. A linked entry is draft evidence, and
|
|
17
17
|
rollups must deduplicate `AttendancePunch.serviceTimeEntryId`.
|
|
18
|
+
`AttendanceSegment` (`attendance_segments`) is a running work interval inside a
|
|
19
|
+
punch: queryable `workRef*`/`taskRef*`/`kind` columns, one open per punch
|
|
20
|
+
(`openSlot`), closed by `startBreak`/`punchOut`/`autoClose`, never overlapping
|
|
21
|
+
breaks or each other, immutable once closed. It creates no `ServiceTimeEntry`.
|
|
22
|
+
Closed punches, breaks and segments change only through
|
|
23
|
+
`AttendanceCorrectionService` (`attendance/correction-service.ts`), which sets a
|
|
24
|
+
per-instance capability (`attendance/write.ts`, not exported) that lifts the
|
|
25
|
+
models' closed-interval checks for one save, validates the whole punch layout,
|
|
26
|
+
recomputes durations and appends an `AttendanceCorrection` (append-only ledger,
|
|
27
|
+
no generated operations; only a first acknowledgement by the subject may update
|
|
28
|
+
a row). Never export `write.ts`. Punch `source`/`deviceRef*` and break `kind` are immutable except through corrections; `autoCloseReason` is write-once via `persistAutoClose`. A confirmed `PeriodTimecard` is honoured by the
|
|
29
|
+
opt-in `confirmedTimecardGuard` (`rollup/correction-guard.ts`), which refuses
|
|
30
|
+
while a card is submitted or confirmed. `PeriodRollupService.reopen()` returns
|
|
31
|
+
such a card to `open` and appends a `TimecardReopening` (`timecard_reopenings`,
|
|
32
|
+
append-only, service-only via `rollup/write.ts`); `submit()` is the optional
|
|
33
|
+
open -> submitted step (`requireSubmission` makes it mandatory) (#3950).
|
|
34
|
+
Segment transitions live in `attendance/segments.ts`; compose them with other
|
|
35
|
+
attendance writes via `AttendanceService.withTransaction`. Any new attendance
|
|
36
|
+
table must be added to the `includeObjects` lists in the `*.optional.test.ts`
|
|
37
|
+
files.
|
|
18
38
|
|
|
19
39
|
## Models
|
|
20
40
|
|
|
@@ -122,3 +142,9 @@ Layer 3 lives under `src/rollup/` and the `./rollup` subpath. See
|
|
|
122
142
|
confirmation, attributed adjustments, isolation, and transaction contracts.
|
|
123
143
|
Timecards and adjustments have closed generated surfaces and service-only
|
|
124
144
|
model writes; never bypass the instance capability with generic persistence.
|
|
145
|
+
|
|
146
|
+
Replay hashes the tap AS SENT (action, device `at`, paid, work, and `source`/`device`/`kind` only when present, so older receipts still match) so retries match;
|
|
147
|
+
`replay(taps, { resolveAt, now, refusal })` only changes the stored time and
|
|
148
|
+
reason. Never hash the resolved time. Work actions (`startWork`, `switchWork`,
|
|
149
|
+
`stopWork`, `resumeWork`) run in the tap's transaction via `apply()`. Refusals store
|
|
150
|
+
`code` from `AttendanceConflict.code`; give new conflicts a code.
|
package/README.md
CHANGED
|
@@ -269,7 +269,7 @@ and are unchanged.
|
|
|
269
269
|
## Attendance
|
|
270
270
|
|
|
271
271
|
The `@happyvertical/smrt-timesheets/attendance` subpath adds `AttendancePunch`,
|
|
272
|
-
`AttendanceBreak`, and `AttendanceService`. Apply migrations for the new tables
|
|
272
|
+
`AttendanceBreak`, `AttendanceSegment`, and `AttendanceService`. Apply migrations for the new tables
|
|
273
273
|
before enabling the module. Existing entry and snapshot tables are unchanged.
|
|
274
274
|
Attendance supports SQLite and PostgreSQL. DuckDB cannot enforce the required
|
|
275
275
|
`ON UPDATE CASCADE` foreign-key contract used by attendance and existing entry
|
|
@@ -288,7 +288,7 @@ reads without replacing the models or changing their schema:
|
|
|
288
288
|
import { ObjectRegistry } from '@happyvertical/smrt-core';
|
|
289
289
|
import '@happyvertical/smrt-timesheets/attendance';
|
|
290
290
|
|
|
291
|
-
for (const model of ['AttendancePunch', 'AttendanceBreak']) {
|
|
291
|
+
for (const model of ['AttendancePunch', 'AttendanceBreak', 'AttendanceSegment']) {
|
|
292
292
|
ObjectRegistry.registerOverride(`@happyvertical/smrt-timesheets:${model}`, {
|
|
293
293
|
api: false, cli: false, mcp: false,
|
|
294
294
|
});
|
|
@@ -321,6 +321,49 @@ pattern per punch. Persisted ownership/start and closed intervals are immutable.
|
|
|
321
321
|
Direct ORM saves serialize against the parent punch and reject a close while a
|
|
322
322
|
break remains open or ends outside the punch.
|
|
323
323
|
|
|
324
|
+
### Running work segments
|
|
325
|
+
|
|
326
|
+
A shift is one punch; inside it a person works on a sequence of things. An
|
|
327
|
+
`AttendanceSegment` (table `attendance_segments`) is one running interval of work
|
|
328
|
+
inside a punch, with queryable columns `workRefType`/`workRefId`, an optional
|
|
329
|
+
second reference `taskRefType`/`taskRefId` (for example an operation; both or
|
|
330
|
+
neither), a short `kind` lane label, `description`, `startedAt`/`endedAt`,
|
|
331
|
+
`durationSeconds` and `closedReason` (`stop`, `switch`, `break`, `punchOut`).
|
|
332
|
+
Indexes on `(tenantId, workRefType, workRefId)` and `(tenantId, taskRefType,
|
|
333
|
+
taskRefId)` serve job costing; a unique `(tenantId, punchId, openSlot)` index
|
|
334
|
+
allows one open segment per punch. Segments never overlap each other or a break.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
await attendance.punchIn(t(8));
|
|
338
|
+
await attendance.startWork(t(8), { workRefType: '@acme:WorkOrder', workRefId: 'WO-1',
|
|
339
|
+
taskRefType: '@acme:Operation', taskRefId: 'SHEAR', description: 'Shear' });
|
|
340
|
+
await attendance.switchWork(t(10), { workRefType: '@acme:WorkOrder', workRefId: 'WO-2',
|
|
341
|
+
taskRefType: '@acme:Operation', taskRefId: 'WELD', description: 'Weld' });
|
|
342
|
+
await attendance.startBreak(t(12)); // closes the segment
|
|
343
|
+
await attendance.endBreak(t(13), { resumeWork: true }); // or attendance.resumeWork(t(13))
|
|
344
|
+
await attendance.punchOut(t(16)); // closes the segment
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`startWork`, `switchWork` (closes the open segment and opens the next at the same
|
|
348
|
+
instant), `stopWork`, `resumeWork` (reopens the work a break interrupted),
|
|
349
|
+
`currentWork()` and `segments(punchId)` are the service surface. `startBreak`,
|
|
350
|
+
`punchOut`, `autoClose` and replayed taps close the open segment. Closed segments
|
|
351
|
+
are immutable; ownership, references and start are immutable always. Closing a
|
|
352
|
+
punch with an open segment, or a segment outside the punch, is refused.
|
|
353
|
+
|
|
354
|
+
Segments are attendance evidence, not service time entries: they do not create
|
|
355
|
+
`ServiceTimeEntry` rows and `punchOut(at, work?)` is unchanged (one draft entry
|
|
356
|
+
for the whole punch; `taskRef*` and `kind` ride in its `metadata`). A consumer that
|
|
357
|
+
costs jobs from segments should not also cost the punch-level entry. Rollups
|
|
358
|
+
still deduplicate `serviceTimeEntryId`.
|
|
359
|
+
|
|
360
|
+
To compose segment work with other attendance mutations (corrections, custom
|
|
361
|
+
replay actions) atomically, use `attendance.withTransaction(async (tx) => ...)`
|
|
362
|
+
and the exported helpers `openSegment`, `listSegments`, `beginSegment`,
|
|
363
|
+
`replaceSegment`, `endSegment`, `continueSegment` and `closeOpenSegment`, which
|
|
364
|
+
take the `AttendanceContext` (`tx.punches`, `tx.breaks`, `tx.segments`, `tx.db`)
|
|
365
|
+
and an open punch.
|
|
366
|
+
|
|
324
367
|
A work reference is optional: attendance alone is not billable evidence. Passing
|
|
325
368
|
one on close, or calling `linkServiceTimeEntry(punch.id, work)` later, creates
|
|
326
369
|
one draft `ServiceTimeEntry` atomically and records `serviceTimeEntryId` on the
|
|
@@ -330,14 +373,30 @@ refused. Approval still uses the entry layer. Rollups must deduplicate this link
|
|
|
330
373
|
not count both attendance and its entry. Zero-net-duration punches cannot create
|
|
331
374
|
service evidence.
|
|
332
375
|
|
|
333
|
-
`autoClose(now,
|
|
334
|
-
|
|
335
|
-
or
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
376
|
+
`autoClose(now, deadline)` closes a due open punch and sets `reviewRequired`.
|
|
377
|
+
`deadline` is either a fixed maximum open duration in seconds (reason
|
|
378
|
+
`maxDuration`) or a per-punch resolver `(punch) => Date | { deadline, reason? }
|
|
379
|
+
| null`, for example the end of the shop day in the shop's time zone or the
|
|
380
|
+
scheduled shift end; `null` leaves the punch open. The open work segment and
|
|
381
|
+
break close at the deadline and the punch records `autoCloseReason` (write-once,
|
|
382
|
+
only through `autoClose`). It returns null before the deadline or when already
|
|
383
|
+
closed, and refuses a resolved deadline before the punch started. If later
|
|
384
|
+
committed attendance would be truncated it refuses the operation for manual
|
|
385
|
+
review. A scheduler calls this per authorized actor; the package does not choose
|
|
386
|
+
overtime, payroll, or shop-time-zone rules.
|
|
387
|
+
|
|
388
|
+
Provenance is optional and consumer-owned (no enum; at most 64 characters, trimmed):
|
|
389
|
+
`punchIn(at, { source, device: { type, id } })` stores `source`, `deviceRefType` and
|
|
390
|
+
`deviceRefId` on the punch (device type and id together, indexed with the tenant), and
|
|
391
|
+
`startBreak(at, paid, { kind })` stores `kind` on the break. They are immutable once
|
|
392
|
+
saved except through `AttendanceCorrectionService` (`addPunch({ source, device })`,
|
|
393
|
+
`saveBreak({ kind })`). All columns default to `''`, so existing rows and calls are unchanged.
|
|
394
|
+
Provenance, `kind` and `autoCloseReason` are additive columns on `attendance_punches` and `attendance_breaks`:
|
|
395
|
+
migrate them as `TEXT NOT NULL DEFAULT ''` (a NULL left on an older row is read as `''`).
|
|
396
|
+
|
|
397
|
+
`replay(taps, options?)` accepts `{ clientId, action, at, paid?, work?, source?, device?, kind? }`, where `action` is
|
|
398
|
+
`punchIn`, `punchOut`, `startBreak`, `endBreak`, or a work action (`startWork`,
|
|
399
|
+
`switchWork` (both require `work`), `stopWork`, `resumeWork`). It sorts each batch by device
|
|
341
400
|
time, then client id. Client ids are durable and scoped to tenant/profile.
|
|
342
401
|
Successful taps set `recordedOffline` and `reviewRequired`. The conflict policy
|
|
343
402
|
is **committed attendance wins**: an event predating committed activity, or
|
|
@@ -349,10 +408,96 @@ are treated as supplied evidence, not server authentication; consumers should
|
|
|
349
408
|
apply device drift policy before replay. Replay receipts must be retained for as
|
|
350
409
|
long as clients can retry their ids.
|
|
351
410
|
|
|
411
|
+
Work actions replay under the same client id and in the same transaction as punch
|
|
412
|
+
and break changes; the outcome carries `segmentId`. Replay options:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
await attendance.replay(taps, {
|
|
416
|
+
// Choose each tap's effective time. Called once per tap without a receipt.
|
|
417
|
+
resolveAt: ({ tap, deviceAt, serverNow, waitedMs, actor }) =>
|
|
418
|
+
waitedMs <= 12 * 3600_000
|
|
419
|
+
? { at: deviceAt, reviewRequired: false }
|
|
420
|
+
: { at: serverNow, reviewRequired: true },
|
|
421
|
+
// Optionally name the stored reason of a refused tap.
|
|
422
|
+
refusal: (tap, error) => ({ code: 'device_busy', message: 'Finish current task first' }),
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The receipt hashes the tap as sent, so a retry with the same `clientId` still
|
|
427
|
+
matches even though the stored time differs; the outcome then has `resolvedAt`.
|
|
428
|
+
Punches are always flagged `reviewRequired`; `reviewRequired: true` from the
|
|
429
|
+
resolver also flags the resulting work segment (`AttendanceSegment.reviewRequired`,
|
|
430
|
+
a new column). A refused outcome stores `{ error, code }`: `code` defaults to the
|
|
431
|
+
`AttendanceConflict.code` (`predates_committed`, `punch_open`, `no_open_punch`,
|
|
432
|
+
`break_open`, `no_open_break`, `during_break`, `work_open`, `no_open_work`,
|
|
433
|
+
`no_resumable_work`, or `conflict`), so a device screen can say why. Resolved times
|
|
434
|
+
that break chronology are refused like any other tap.
|
|
435
|
+
|
|
436
|
+
### Correcting closed attendance
|
|
437
|
+
|
|
438
|
+
Closed punches, breaks and segments are immutable to every direct write.
|
|
439
|
+
`AttendanceCorrectionService` is the one sanctioned path around that, and it
|
|
440
|
+
leaves a paper trail. Construct it with the **correcting** person as `actor`;
|
|
441
|
+
each request names the `subjectProfileId` whose time it is. As with
|
|
442
|
+
`AttendanceService`, your application authorizes the actor (and decides whether
|
|
443
|
+
they may correct this subject) first.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
import { AttendanceCorrectionService } from '@happyvertical/smrt-timesheets/attendance';
|
|
447
|
+
import { confirmedTimecardGuard } from '@happyvertical/smrt-timesheets/rollup';
|
|
448
|
+
|
|
449
|
+
const fix = new AttendanceCorrectionService(
|
|
450
|
+
db,
|
|
451
|
+
{ tenantId, profileId: foremanId },
|
|
452
|
+
{ guard: confirmedTimecardGuard }, // optional, see below
|
|
453
|
+
);
|
|
454
|
+
const base = { subjectProfileId: welderId, reason: 'forgot to clock out' };
|
|
455
|
+
|
|
456
|
+
await fix.correctPunch({ ...base, punchId, endedAt: new Date('2026-10-01T15:30:00Z'),
|
|
457
|
+
acknowledgementRequired: true });
|
|
458
|
+
await fix.addPunch({ ...base, reason: 'missed punch', startedAt, endedAt });
|
|
459
|
+
await fix.saveBreak({ ...base, punchId, startedAt, endedAt, paid: false }); // add
|
|
460
|
+
await fix.saveBreak({ ...base, punchId, breakId, endedAt }); // change
|
|
461
|
+
await fix.removeBreak({ ...base, breakId });
|
|
462
|
+
await fix.saveSegment({ ...base, punchId, startedAt, endedAt, work }); // add
|
|
463
|
+
await fix.saveSegment({ ...base, punchId, segmentId, endedAt }); // change
|
|
464
|
+
await fix.removeSegment({ ...base, segmentId });
|
|
465
|
+
|
|
466
|
+
// the welder, constructing the service as themselves:
|
|
467
|
+
await new AttendanceCorrectionService(db, { tenantId, profileId: welderId })
|
|
468
|
+
.acknowledge(correctionId);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Every method runs in one transaction serialized with the subject's own taps. It
|
|
472
|
+
validates the resulting layout (everything inside its punch, no overlapping
|
|
473
|
+
breaks, segments or other punches, no segment across a break), recomputes
|
|
474
|
+
`durationSeconds` and `unpaidBreakSeconds`, and appends an `AttendanceCorrection`
|
|
475
|
+
row (`attendance_corrections`: `kind`, `subjectProfileId`, `actorProfileId`,
|
|
476
|
+
`punchId`, `targetId`, `beforeState`/`afterState` JSON, a required non-blank
|
|
477
|
+
`reason`, `acknowledgementRequired`, `acknowledgedAt`/`acknowledgedByProfileId`).
|
|
478
|
+
Rows are append-only: the only later change is the subject's single
|
|
479
|
+
`acknowledge()`, and `listForPunch(punchId)` / `pendingAcknowledgements()` read
|
|
480
|
+
the ledger. The ledger has no generated operations, and every ledger,
|
|
481
|
+
closed-break and closed-segment write (insert, acknowledge, delete) is
|
|
482
|
+
service-only: a generic `save()`, `delete()` or collection `create()` is refused. Notes:
|
|
483
|
+
|
|
484
|
+
- Only **closed** punches are corrected; an open one is `AttendanceService`'s.
|
|
485
|
+
Moving a punch edge past a break or segment is refused: correct those first.
|
|
486
|
+
- `correctPunch` clears `reviewRequired` (a person reviewed it). A linked
|
|
487
|
+
*draft* `ServiceTimeEntry` is kept in step; a submitted or approved one
|
|
488
|
+
refuses the correction until it is corrected with `ServiceEvidenceService`.
|
|
489
|
+
- **Confirmed timecards.** A confirmed `PeriodTimecard` never changes, so
|
|
490
|
+
correcting its sources would leave card and attendance disagreeing for good.
|
|
491
|
+
Pass `confirmedTimecardGuard` (from `./rollup`) and a correction overlapping a
|
|
492
|
+
confirmed (or submitted) card is refused before any write; call
|
|
493
|
+
`PeriodRollupService.reopen()` first, correct, then `rollup()` again. Any other lock can be supplied as `guard`.
|
|
494
|
+
- Closed `AttendanceBreak`/`AttendanceSegment` rows can no longer be deleted
|
|
495
|
+
directly; `removeBreak`/`removeSegment` are the path.
|
|
496
|
+
|
|
352
497
|
## Period timecards
|
|
353
498
|
|
|
354
499
|
`./rollup` provides per-person integer-second regular/overtime rollups, immutable
|
|
355
|
-
confirmation, and attributed append-only adjustments. Consumer resolvers own
|
|
500
|
+
confirmation (with an optional submitted step), audited reopening, and attributed append-only adjustments. Consumer resolvers own
|
|
356
501
|
calendar/timezone, holiday and overtime rules. See the [contract and example](agents/period-rollup.md).
|
|
357
502
|
|
|
358
503
|
## Effective-dated labour pricing
|
package/agents/period-rollup.md
CHANGED
|
@@ -133,3 +133,23 @@ embedded writer serialization or a PostgreSQL tenant/person advisory lock.
|
|
|
133
133
|
Outputs are reloaded on the root executor after commit. Rollup reads are an
|
|
134
134
|
application snapshot: subsequent source approvals/corrections appear on the
|
|
135
135
|
next open rollup, and confirmed cards require explicit adjustments.
|
|
136
|
+
|
|
137
|
+
## Lifecycle: submit, confirm, reopen (#3950)
|
|
138
|
+
|
|
139
|
+
`open` (draft) -> `submitted` (optional) -> `confirmed`. `submit(id)` freezes an
|
|
140
|
+
open card for review; `confirm(id)` accepts an open card too unless the service
|
|
141
|
+
is built with `{ requireSubmission: true }`. Submitted and confirmed cards are
|
|
142
|
+
not re-rolled.
|
|
143
|
+
|
|
144
|
+
`reopen(id, { operationId, reason })` returns a submitted or confirmed card to
|
|
145
|
+
`open` (clearing its submission/confirmation attribution) and appends a
|
|
146
|
+
`TimecardReopening`: actor, reason, prior status and a JSON snapshot of the
|
|
147
|
+
previous base, adjustment and effective seconds (`getPrevious()`). It is
|
|
148
|
+
idempotent per `operationId` and refuses a blank reason. `reopenings(id)` lists
|
|
149
|
+
the history. The consumer authorizes the actor; give reopening its own
|
|
150
|
+
permission. Existing adjustments stay and keep counting in `totals()`: after a
|
|
151
|
+
re-roll, offset ones the correction made redundant with a new adjustment.
|
|
152
|
+
`confirmedTimecardGuard` refuses corrections while a card is submitted or
|
|
153
|
+
confirmed and allows them once it is reopened. The new `timecard_reopenings`
|
|
154
|
+
table, and nullable `submitted_at`/`submitted_by_profile_id` columns on
|
|
155
|
+
`period_timecards`, need a migration.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import type { AttendanceBreakCollection, AttendancePunchCollection, AttendanceReplayCollection, AttendanceSegmentCollection } from './models.js';
|
|
3
|
+
/** Resolve these identifiers from the authenticated session, never from tap input. */
|
|
4
|
+
export interface AttendanceActor {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
profileId: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Explicit work attribution for a closed punch's draft evidence entry or for a
|
|
10
|
+
* running work segment. `taskRefType`/`taskRefId` (both or neither) name an
|
|
11
|
+
* optional second reference such as an operation; `kind` is an optional short
|
|
12
|
+
* consumer lane label.
|
|
13
|
+
*/
|
|
14
|
+
export interface AttendanceWork {
|
|
15
|
+
workRefType: string;
|
|
16
|
+
workRefId: string;
|
|
17
|
+
description: string;
|
|
18
|
+
taskRefType?: string;
|
|
19
|
+
taskRefId?: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Where a punch was captured: a consumer-owned type (for example a class name
|
|
24
|
+
* such as a station or tablet) and that device's id. Both or neither.
|
|
25
|
+
*/
|
|
26
|
+
export interface AttendanceDeviceRef {
|
|
27
|
+
type: string;
|
|
28
|
+
id: string;
|
|
29
|
+
}
|
|
30
|
+
/** Optional provenance recorded when a punch opens. */
|
|
31
|
+
export interface AttendancePunchOptions {
|
|
32
|
+
/** Short consumer-owned label such as `device`, `web`, `import`, `correction`. */
|
|
33
|
+
source?: string;
|
|
34
|
+
/** The device that captured the punch. */
|
|
35
|
+
device?: AttendanceDeviceRef;
|
|
36
|
+
}
|
|
37
|
+
/** Optional detail recorded when a break starts. */
|
|
38
|
+
export interface AttendanceBreakOptions {
|
|
39
|
+
/** Short consumer-owned label such as `meal` or `rest`. */
|
|
40
|
+
kind?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Validate an optional short label (`source`, `kind`, device type); returns the stored form. */
|
|
43
|
+
export declare function attendanceLabel(value: unknown, name: string): string;
|
|
44
|
+
/** Validate punch provenance; returns the stored column values. */
|
|
45
|
+
export declare function attendanceProvenance(options?: AttendancePunchOptions): {
|
|
46
|
+
source: string;
|
|
47
|
+
deviceRefType: string;
|
|
48
|
+
deviceRefId: string;
|
|
49
|
+
};
|
|
50
|
+
/** Expected state conflict, distinct from retryable database failures. */
|
|
51
|
+
export declare class AttendanceConflict extends Error {
|
|
52
|
+
/**
|
|
53
|
+
* Stable machine-readable reason (see {@link AttendanceRefusalCode}); stored
|
|
54
|
+
* on a refused replay outcome so a device screen can say why.
|
|
55
|
+
*/
|
|
56
|
+
readonly code: AttendanceRefusalCode;
|
|
57
|
+
constructor(message: string, code?: AttendanceRefusalCode);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Built-in refusal codes. Consumers may store any other string through the
|
|
61
|
+
* `refusalCode` replay hook.
|
|
62
|
+
*/
|
|
63
|
+
export type AttendanceRefusalCode = 'conflict' | 'invalid' | 'predates_committed' | 'punch_open' | 'no_open_punch' | 'break_open' | 'no_open_break' | 'during_break' | 'work_open' | 'no_open_work' | 'no_resumable_work' | (string & {});
|
|
64
|
+
/**
|
|
65
|
+
* Collections bound to one transaction executor. Handed to
|
|
66
|
+
* `AttendanceService.withTransaction` callbacks and to the segment helpers so
|
|
67
|
+
* corrections and replay can compose several mutations atomically.
|
|
68
|
+
*/
|
|
69
|
+
export type AttendanceContext = {
|
|
70
|
+
db: DatabaseInterface;
|
|
71
|
+
punches: AttendancePunchCollection;
|
|
72
|
+
breaks: AttendanceBreakCollection;
|
|
73
|
+
segments: AttendanceSegmentCollection;
|
|
74
|
+
replays: AttendanceReplayCollection;
|
|
75
|
+
};
|
|
76
|
+
/** Validate a work reference; throws {@link AttendanceConflict}. */
|
|
77
|
+
export declare function validateAttendanceWork(work: AttendanceWork): void;
|
|
78
|
+
/** Canonicalize a tenant/profile id: PostgreSQL requires UUIDs, other adapters take it verbatim. */
|
|
79
|
+
export declare function attendanceIdentity(db: DatabaseInterface, value: string): string;
|
|
80
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/attendance/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,aAAa,CAAC;AAErB,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,uDAAuD;AACvD,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B;AAED,oDAAoD;AACpD,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAID,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpE;AAED,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,CAaA;AAED,0EAA0E;AAC1E,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;gBACzB,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,qBAAkC;CAItE;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAC7B,UAAU,GACV,SAAS,GACT,oBAAoB,GACpB,YAAY,GACZ,eAAe,GACf,YAAY,GACZ,eAAe,GACf,cAAc,GACd,WAAW,GACX,cAAc,GACd,mBAAmB,GACnB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,iBAAiB,CAAC;IACtB,OAAO,EAAE,yBAAyB,CAAC;IACnC,MAAM,EAAE,yBAAyB,CAAC;IAClC,QAAQ,EAAE,2BAA2B,CAAC;IACtC,OAAO,EAAE,0BAA0B,CAAC;CACrC,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAmBjE;AAED,oGAAoG;AACpG,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,MAAM,GACZ,MAAM,CAMR"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';
|
|
2
|
+
/** What a correction did. Punch kinds target the punch itself. */
|
|
3
|
+
export declare const ATTENDANCE_CORRECTION_KINDS: readonly ["punch.corrected", "punch.added", "break.added", "break.changed", "break.removed", "segment.added", "segment.changed", "segment.removed"];
|
|
4
|
+
export type AttendanceCorrectionKind = (typeof ATTENDANCE_CORRECTION_KINDS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* One append-only change to closed attendance: who changed whose time, why, and
|
|
7
|
+
* the before/after values as JSON (`null` for an addition's before or a
|
|
8
|
+
* removal's after). Written only by `AttendanceCorrectionService`, in the same
|
|
9
|
+
* transaction as the change. After insert only the acknowledgement may be set,
|
|
10
|
+
* once, by the subject; rows are never deleted.
|
|
11
|
+
*/
|
|
12
|
+
export declare class AttendanceCorrection extends SmrtObject {
|
|
13
|
+
tenantId: string;
|
|
14
|
+
/** Whose time was corrected. */
|
|
15
|
+
subjectProfileId: string;
|
|
16
|
+
/** Who made the correction. */
|
|
17
|
+
actorProfileId: string;
|
|
18
|
+
kind: AttendanceCorrectionKind;
|
|
19
|
+
punchId: string;
|
|
20
|
+
/** The break or segment id for those kinds; the punch id for punch kinds. */
|
|
21
|
+
targetId: string;
|
|
22
|
+
beforeState: string;
|
|
23
|
+
afterState: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
acknowledgementRequired: boolean;
|
|
26
|
+
acknowledgedAt: Date | null;
|
|
27
|
+
acknowledgedByProfileId: string | null;
|
|
28
|
+
/** The values before the change, or null for an addition. */
|
|
29
|
+
getBefore<T = Record<string, unknown>>(): T | null;
|
|
30
|
+
/** The values after the change, or null for a removal. */
|
|
31
|
+
getAfter<T = Record<string, unknown>>(): T | null;
|
|
32
|
+
save(): Promise<this>;
|
|
33
|
+
delete(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** The correction ledger. */
|
|
36
|
+
export declare class AttendanceCorrectionCollection extends SmrtCollection<AttendanceCorrection> {
|
|
37
|
+
static readonly _itemClass: typeof AttendanceCorrection;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=correction-models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correction-models.d.ts","sourceRoot":"","sources":["../../src/attendance/correction-models.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,cAAc,EACd,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAMlC,kEAAkE;AAClE,eAAO,MAAM,2BAA2B,qJAS9B,CAAC;AACX,MAAM,MAAM,wBAAwB,GAClC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAc/C;;;;;;GAMG;AACH,qBAkBa,oBAAqB,SAAQ,UAAU;IACtC,QAAQ,SAAM;IAC1B,gCAAgC;IAEhC,gBAAgB,SAAM;IACtB,+BAA+B;IAE/B,cAAc,SAAM;IACK,IAAI,EAAE,wBAAwB,CAAqB;IAC/C,OAAO,SAAM;IAC1C,6EAA6E;IAC7E,QAAQ,SAAM;IACW,WAAW,SAAU;IACrB,UAAU,SAAU;IACpB,MAAM,SAAM;IACrC,uBAAuB,UAAS;IAChC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEnC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE9C,6DAA6D;IAC7D,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI;IAGlD,0DAA0D;IAC1D,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI;IAIlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAsDrB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAGvC;AAED,6BAA6B;AAC7B,qBAAa,8BAA+B,SAAQ,cAAc,CAAC,oBAAoB,CAAC;IACtF,MAAM,CAAC,QAAQ,CAAC,UAAU,8BAAwB;CACnD"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { type AttendanceActor, type AttendanceContext, type AttendancePunchOptions, type AttendanceWork } from './context.js';
|
|
3
|
+
import { AttendanceCorrection } from './correction-models.js';
|
|
4
|
+
/** A closed time range a correction touches, for period-lock checks. */
|
|
5
|
+
export interface AttendanceCorrectionInterval {
|
|
6
|
+
startsAt: Date;
|
|
7
|
+
endsAt: Date;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Optional period lock, run on the correction's transaction before any write.
|
|
11
|
+
* Return a message to refuse the correction (for example "overlaps a confirmed
|
|
12
|
+
* timecard"), or null to allow it. `confirmedTimecardGuard` in `./rollup` is the
|
|
13
|
+
* ready-made one.
|
|
14
|
+
*/
|
|
15
|
+
export type AttendanceCorrectionGuard = (tx: AttendanceContext, subject: AttendanceActor, intervals: readonly AttendanceCorrectionInterval[]) => Promise<string | null>;
|
|
16
|
+
export interface AttendanceCorrectionOptions {
|
|
17
|
+
guard?: AttendanceCorrectionGuard;
|
|
18
|
+
}
|
|
19
|
+
/** Fields every correction carries. */
|
|
20
|
+
export interface AttendanceCorrectionRequest {
|
|
21
|
+
/** Whose time is being corrected. */
|
|
22
|
+
subjectProfileId: string;
|
|
23
|
+
/** Required, non-blank. Stored verbatim (trimmed) on the ledger row. */
|
|
24
|
+
reason: string;
|
|
25
|
+
/** When true the subject must `acknowledge()` the correction. */
|
|
26
|
+
acknowledgementRequired?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface CorrectPunchInput extends AttendanceCorrectionRequest {
|
|
29
|
+
punchId: string;
|
|
30
|
+
startedAt?: Date;
|
|
31
|
+
endedAt?: Date;
|
|
32
|
+
}
|
|
33
|
+
export interface AddPunchInput extends AttendanceCorrectionRequest, AttendancePunchOptions {
|
|
34
|
+
startedAt: Date;
|
|
35
|
+
endedAt: Date;
|
|
36
|
+
}
|
|
37
|
+
/** Adds a break when `breakId` is absent; otherwise changes that break. */
|
|
38
|
+
export interface SaveBreakInput extends AttendanceCorrectionRequest {
|
|
39
|
+
punchId: string;
|
|
40
|
+
breakId?: string;
|
|
41
|
+
startedAt?: Date;
|
|
42
|
+
endedAt?: Date;
|
|
43
|
+
paid?: boolean;
|
|
44
|
+
/** Short consumer-owned label (for example `meal`); omit to keep, `''` to clear. */
|
|
45
|
+
kind?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RemoveBreakInput extends AttendanceCorrectionRequest {
|
|
48
|
+
breakId: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Adds a segment when `segmentId` is absent (`startedAt`, `endedAt` and `work`
|
|
52
|
+
* are then required); otherwise changes that segment, keeping what is omitted.
|
|
53
|
+
*/
|
|
54
|
+
export interface SaveSegmentInput extends AttendanceCorrectionRequest {
|
|
55
|
+
punchId: string;
|
|
56
|
+
segmentId?: string;
|
|
57
|
+
startedAt?: Date;
|
|
58
|
+
endedAt?: Date;
|
|
59
|
+
work?: AttendanceWork;
|
|
60
|
+
}
|
|
61
|
+
export interface RemoveSegmentInput extends AttendanceCorrectionRequest {
|
|
62
|
+
segmentId: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Corrections to closed attendance, with a paper trail.
|
|
66
|
+
*
|
|
67
|
+
* `actor` is who makes the change; each request names the `subjectProfileId`
|
|
68
|
+
* whose time it is. The application authorizes the actor (and decides whether
|
|
69
|
+
* they may correct this subject) before constructing the service, exactly as
|
|
70
|
+
* for `AttendanceService`. This service is the only path that may change a
|
|
71
|
+
* closed punch, break or segment: each method validates the resulting layout
|
|
72
|
+
* (everything inside its punch, no overlapping breaks, segments or punches,
|
|
73
|
+
* no segment across a break), recomputes `durationSeconds` and
|
|
74
|
+
* `unpaidBreakSeconds`, keeps a still-draft linked service entry in step, and
|
|
75
|
+
* appends an `AttendanceCorrection` row, all in one transaction serialized with
|
|
76
|
+
* the subject's own taps. Open punches are not corrected here: use
|
|
77
|
+
* `AttendanceService`.
|
|
78
|
+
*/
|
|
79
|
+
export declare class AttendanceCorrectionService {
|
|
80
|
+
private readonly db;
|
|
81
|
+
private readonly options;
|
|
82
|
+
private readonly actor;
|
|
83
|
+
constructor(db: DatabaseInterface, actor: AttendanceActor, options?: AttendanceCorrectionOptions);
|
|
84
|
+
/** Change a closed punch's start and/or end. A reviewed punch no longer needs review. */
|
|
85
|
+
correctPunch(input: CorrectPunchInput): Promise<AttendanceCorrection>;
|
|
86
|
+
/** Record a missed punch retroactively, as an already-closed interval. */
|
|
87
|
+
addPunch(input: AddPunchInput): Promise<AttendanceCorrection>;
|
|
88
|
+
/** Add a break to a closed punch, or change one (times and/or paid). */
|
|
89
|
+
saveBreak(input: SaveBreakInput): Promise<AttendanceCorrection>;
|
|
90
|
+
/** Remove a break from a closed punch. */
|
|
91
|
+
removeBreak(input: RemoveBreakInput): Promise<AttendanceCorrection>;
|
|
92
|
+
/** Add a work segment to a closed punch, or change one (times and/or work attribution). */
|
|
93
|
+
saveSegment(input: SaveSegmentInput): Promise<AttendanceCorrection>;
|
|
94
|
+
/** Remove a work segment from a closed punch. */
|
|
95
|
+
removeSegment(input: RemoveSegmentInput): Promise<AttendanceCorrection>;
|
|
96
|
+
/**
|
|
97
|
+
* The subject confirms they have seen a correction that required it. Only
|
|
98
|
+
* the subject (this service's actor) may; it can be done once.
|
|
99
|
+
*/
|
|
100
|
+
acknowledge(correctionId: string, at?: Date): Promise<AttendanceCorrection>;
|
|
101
|
+
/** Corrections to one punch, oldest first. */
|
|
102
|
+
listForPunch(punchId: string): Promise<AttendanceCorrection[]>;
|
|
103
|
+
/** Corrections of the actor's own time that still await their acknowledgement. */
|
|
104
|
+
pendingAcknowledgements(): Promise<AttendanceCorrection[]>;
|
|
105
|
+
private attendance;
|
|
106
|
+
private read;
|
|
107
|
+
private run;
|
|
108
|
+
private closedPunch;
|
|
109
|
+
private children;
|
|
110
|
+
private layout;
|
|
111
|
+
private attribute;
|
|
112
|
+
private guard;
|
|
113
|
+
/** The punch's still-draft evidence entry, refusing when it is no longer editable. */
|
|
114
|
+
private editableEntry;
|
|
115
|
+
/** Recompute the punch's durations from its breaks and persist it and its draft entry. */
|
|
116
|
+
private settle;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=correction-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correction-service.d.ts","sourceRoot":"","sources":["../../src/attendance/correction-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,7 +8,14 @@ export declare class AttendancePunch extends SmrtObject {
|
|
|
8
8
|
durationSeconds: number;
|
|
9
9
|
unpaidBreakSeconds: number;
|
|
10
10
|
reviewRequired: boolean;
|
|
11
|
+
/** Why the scheduler closed this punch; empty unless `autoClose` did. Write-once. */
|
|
12
|
+
autoCloseReason: string;
|
|
11
13
|
recordedOffline: boolean;
|
|
14
|
+
/** Optional short consumer-owned capture label (`device`, `web`, `import`, ...); `''` when unknown. */
|
|
15
|
+
source: string;
|
|
16
|
+
/** Qualified type of the device that captured the punch; both device columns or neither. */
|
|
17
|
+
deviceRefType: string;
|
|
18
|
+
deviceRefId: string;
|
|
12
19
|
serviceTimeEntryId: string | null;
|
|
13
20
|
/** Unique while open; closed rows retain independent non-null slots on every dialect. */
|
|
14
21
|
openSlot: string;
|
|
@@ -22,15 +29,60 @@ export declare class AttendanceBreak extends SmrtObject {
|
|
|
22
29
|
startedAt: Date;
|
|
23
30
|
endedAt: Date | null;
|
|
24
31
|
paid: boolean;
|
|
32
|
+
/** Optional short consumer-owned label (`meal`, `rest`, ...); `''` when unspecified. */
|
|
33
|
+
kind: string;
|
|
25
34
|
openSlot: string;
|
|
26
35
|
save(): Promise<this>;
|
|
27
36
|
private saveConsistent;
|
|
37
|
+
delete(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
declare const SEGMENT_CLOSE_REASONS: readonly ["stop", "switch", "break", "punchOut"];
|
|
40
|
+
/** Why a work segment ended; `''` while it is open. */
|
|
41
|
+
export type AttendanceSegmentCloseReason = (typeof SEGMENT_CLOSE_REASONS)[number] | '';
|
|
42
|
+
/**
|
|
43
|
+
* A running unit of work inside one attendance punch: a work reference plus an
|
|
44
|
+
* optional second (task) reference as queryable columns, so job costing can
|
|
45
|
+
* index and join them. At most one segment is open per punch (`openSlot`). A
|
|
46
|
+
* segment never spans a break; `startBreak` and `punchOut` close it. Segments
|
|
47
|
+
* are attendance evidence and are not themselves service time entries.
|
|
48
|
+
*/
|
|
49
|
+
export declare class AttendanceSegment extends SmrtObject {
|
|
50
|
+
tenantId: string;
|
|
51
|
+
punchId: string;
|
|
52
|
+
/** Qualified class name of the work context (for example a work order). */
|
|
53
|
+
workRefType: string;
|
|
54
|
+
workRefId: string;
|
|
55
|
+
/** Optional second reference (for example an operation); both or neither. */
|
|
56
|
+
taskRefType: string;
|
|
57
|
+
taskRefId: string;
|
|
58
|
+
/** Optional short consumer lane label. */
|
|
59
|
+
kind: string;
|
|
60
|
+
description: string;
|
|
61
|
+
startedAt: Date;
|
|
62
|
+
endedAt: Date | null;
|
|
63
|
+
/** Elapsed whole seconds, set when the segment closes. */
|
|
64
|
+
durationSeconds: number;
|
|
65
|
+
/** `stop`, `switch`, `break` or `punchOut` once closed; `''` while open. */
|
|
66
|
+
closedReason: AttendanceSegmentCloseReason;
|
|
67
|
+
/** Set when a replay time policy asked for review of this segment's boundary. */
|
|
68
|
+
reviewRequired: boolean;
|
|
69
|
+
openSlot: string;
|
|
70
|
+
save(): Promise<this>;
|
|
71
|
+
private saveConsistent;
|
|
72
|
+
delete(): Promise<void>;
|
|
28
73
|
}
|
|
29
74
|
/** Durable success or conflict; conflicts do not mutate attendance. */
|
|
30
75
|
export interface AttendanceReplayOutcome {
|
|
31
76
|
clientId: string;
|
|
32
77
|
punchId?: string;
|
|
78
|
+
/** Work segment a replayed work action started, switched to, stopped or resumed. */
|
|
79
|
+
segmentId?: string;
|
|
80
|
+
/** Effective time stored when a `resolveAt` policy changed it (ISO 8601). */
|
|
81
|
+
resolvedAt?: string;
|
|
82
|
+
/** Refusal message (with `code`); present exactly when the tap was refused. */
|
|
33
83
|
error?: string;
|
|
84
|
+
/** Typed refusal reason; see `AttendanceRefusalCode`. */
|
|
85
|
+
code?: string;
|
|
34
86
|
}
|
|
35
87
|
/** Durable offline-tap outcome; never exposed through generated operations. */
|
|
36
88
|
export declare class AttendanceReplay extends SmrtObject {
|
|
@@ -52,8 +104,13 @@ export declare class AttendancePunchCollection extends SmrtCollection<Attendance
|
|
|
52
104
|
export declare class AttendanceBreakCollection extends SmrtCollection<AttendanceBreak> {
|
|
53
105
|
static readonly _itemClass: typeof AttendanceBreak;
|
|
54
106
|
}
|
|
107
|
+
/** Work segments for job costing and attendance review. */
|
|
108
|
+
export declare class AttendanceSegmentCollection extends SmrtCollection<AttendanceSegment> {
|
|
109
|
+
static readonly _itemClass: typeof AttendanceSegment;
|
|
110
|
+
}
|
|
55
111
|
/** Internal durable replay ledger. */
|
|
56
112
|
export declare class AttendanceReplayCollection extends SmrtCollection<AttendanceReplay> {
|
|
57
113
|
static readonly _itemClass: typeof AttendanceReplay;
|
|
58
114
|
}
|
|
115
|
+
export {};
|
|
59
116
|
//# sourceMappingURL=models.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/attendance/models.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,cAAc,EACd,UAAU,EAGX,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/attendance/models.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AttendanceContext, type AttendanceWork } from './context.js';
|
|
2
|
+
import type { AttendancePunch, AttendanceSegment, AttendanceSegmentCloseReason } from './models.js';
|
|
3
|
+
/** The open segment of a punch, or null. */
|
|
4
|
+
export declare function openSegment(tx: AttendanceContext, punch: AttendancePunch): Promise<AttendanceSegment | null>;
|
|
5
|
+
/** All segments of a punch, oldest first. */
|
|
6
|
+
export declare function listSegments(tx: AttendanceContext, punch: AttendancePunch): Promise<AttendanceSegment[]>;
|
|
7
|
+
/** Close the punch's open segment, if any. Returns it, or null when none was open. */
|
|
8
|
+
export declare function closeOpenSegment(tx: AttendanceContext, punch: AttendancePunch, at: Date, reason: Exclude<AttendanceSegmentCloseReason, ''>): Promise<AttendanceSegment | null>;
|
|
9
|
+
/** Open a segment; refuses while a break or another segment is open. */
|
|
10
|
+
export declare function beginSegment(tx: AttendanceContext, punch: AttendancePunch, at: Date, work: AttendanceWork): Promise<AttendanceSegment>;
|
|
11
|
+
/** Atomically close the open segment and open the next at the same instant. */
|
|
12
|
+
export declare function replaceSegment(tx: AttendanceContext, punch: AttendancePunch, at: Date, work: AttendanceWork): Promise<AttendanceSegment>;
|
|
13
|
+
/** Close the open segment (`stop`); refuses when none is open. */
|
|
14
|
+
export declare function endSegment(tx: AttendanceContext, punch: AttendancePunch, at: Date): Promise<AttendanceSegment>;
|
|
15
|
+
/**
|
|
16
|
+
* Reopen the work a break interrupted: a new segment with the same references
|
|
17
|
+
* as the latest segment, which must have been closed by `startBreak`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function continueSegment(tx: AttendanceContext, punch: AttendancePunch, at: Date): Promise<AttendanceSegment>;
|
|
20
|
+
//# sourceMappingURL=segments.d.ts.map
|