@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.
Files changed (53) hide show
  1. package/AGENTS.md +26 -0
  2. package/README.md +156 -11
  3. package/agents/period-rollup.md +20 -0
  4. package/dist/attendance/context.d.ts +80 -0
  5. package/dist/attendance/context.d.ts.map +1 -0
  6. package/dist/attendance/correction-models.d.ts +39 -0
  7. package/dist/attendance/correction-models.d.ts.map +1 -0
  8. package/dist/attendance/correction-service.d.ts +118 -0
  9. package/dist/attendance/correction-service.d.ts.map +1 -0
  10. package/dist/attendance/models.d.ts +57 -0
  11. package/dist/attendance/models.d.ts.map +1 -1
  12. package/dist/attendance/segments.d.ts +20 -0
  13. package/dist/attendance/segments.d.ts.map +1 -0
  14. package/dist/attendance/service.d.ts +106 -22
  15. package/dist/attendance/service.d.ts.map +1 -1
  16. package/dist/attendance/write.d.ts +12 -0
  17. package/dist/attendance/write.d.ts.map +1 -0
  18. package/dist/attendance.d.ts +2 -0
  19. package/dist/attendance.d.ts.map +1 -1
  20. package/dist/attendance.js +3 -323
  21. package/dist/chunks/{attendance-BVeStEBv.js → attendance-B1jCbqSb.js} +3 -3
  22. package/dist/chunks/{attendance-BVeStEBv.js.map → attendance-B1jCbqSb.js.map} +1 -1
  23. package/dist/chunks/attendance-DGVy-iHI.js +1246 -0
  24. package/dist/chunks/attendance-DGVy-iHI.js.map +1 -0
  25. package/dist/chunks/models-0-YD5F_o.js +372 -0
  26. package/dist/chunks/models-0-YD5F_o.js.map +1 -0
  27. package/dist/chunks/{rollup-U1IO_CMA.js → rollup-CDqzCxI2.js} +211 -6
  28. package/dist/chunks/rollup-CDqzCxI2.js.map +1 -0
  29. package/dist/chunks/service-time-entry-t7P-bkOQ.js +227 -0
  30. package/dist/chunks/service-time-entry-t7P-bkOQ.js.map +1 -0
  31. package/dist/index.js +5 -5
  32. package/dist/manifest.json +2965 -1358
  33. package/dist/rollup/correction-guard.d.ts +12 -0
  34. package/dist/rollup/correction-guard.d.ts.map +1 -0
  35. package/dist/rollup/models.d.ts +31 -1
  36. package/dist/rollup/models.d.ts.map +1 -1
  37. package/dist/rollup/service.d.ts +25 -7
  38. package/dist/rollup/service.d.ts.map +1 -1
  39. package/dist/rollup/types.d.ts +18 -0
  40. package/dist/rollup/types.d.ts.map +1 -1
  41. package/dist/rollup/write.d.ts +2 -0
  42. package/dist/rollup/write.d.ts.map +1 -1
  43. package/dist/rollup.d.ts +3 -2
  44. package/dist/rollup.d.ts.map +1 -1
  45. package/dist/rollup.js +2 -2
  46. package/dist/smrt-knowledge.json +700 -34
  47. package/package.json +7 -7
  48. package/dist/attendance.js.map +0 -1
  49. package/dist/chunks/models-zU8Um7c2.js +0 -176
  50. package/dist/chunks/models-zU8Um7c2.js.map +0 -1
  51. package/dist/chunks/rollup-U1IO_CMA.js.map +0 -1
  52. package/dist/chunks/service-time-entry-CBv-FF-W.js +0 -227
  53. 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, maxOpenSeconds)` closes a due open punch at its configured
334
- maximum duration and sets `reviewRequired`. It returns null before the deadline
335
- or when already closed. If later committed attendance would be truncated it
336
- refuses the operation for manual review. A scheduler calls this per authorized
337
- actor; the package does not choose overtime, payroll, or shop-time-zone rules.
338
-
339
- `replay(taps)` accepts `{ clientId, action, at, paid?, work? }`, where `action` is
340
- `punchIn`, `punchOut`, `startBreak`, or `endBreak`. It sorts each batch by device
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
@@ -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;AA4BlC,4EAA4E;AAC5E,qBAca,eAAgB,SAAQ,UAAU;IACjC,QAAQ,SAAM;IAC+B,SAAS,SAAM;IACxE,SAAS,EAAE,IAAI,CAAc;IAC7B,OAAO,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC5B,eAAe,SAAK;IACpB,kBAAkB,SAAK;IACvB,cAAc,UAAS;IACvB,eAAe,UAAS;IAE4B,kBAAkB,EAClE,MAAM,GACN,IAAI,CAAQ;IAChB,yFAAyF;IAC9D,QAAQ,SAAM;IAE1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAMtB,cAAc;CA0D7B;AAED,0DAA0D;AAC1D,qBAca,eAAgB,SAAQ,UAAU;IACjC,QAAQ,SAAM;IACG,OAAO,SAAM;IAC1C,SAAS,EAAE,IAAI,CAAc;IAC7B,OAAO,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC5B,IAAI,UAAS;IACc,QAAQ,SAAM;IAE1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAMtB,cAAc;CA6C7B;AAED,uEAAuE;AACvE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiBD,+EAA+E;AAC/E,qBAQa,gBAAiB,SAAQ,UAAU;IAClC,QAAQ,SAAM;IACe,SAAS,SAAM;IAC/B,OAAO,SAAM;IACb,OAAO,SAAQ;IAExC,qFAAqF;IACrF,UAAU,IAAI,uBAAuB;IAOrC,uEAAuE;IACvE,UAAU,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI;IAMjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC;AAED,gDAAgD;AAChD,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;CAC9C;AACD,4DAA4D;AAC5D,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;CAC9C;AACD,sCAAsC;AACtC,qBAAa,0BAA2B,SAAQ,cAAc,CAAC,gBAAgB,CAAC;IAC9E,MAAM,CAAC,QAAQ,CAAC,UAAU,0BAAoB;CAC/C"}
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;AA6BlC,4EAA4E;AAC5E,qBAmBa,eAAgB,SAAQ,UAAU;IACjC,QAAQ,SAAM;IAC+B,SAAS,SAAM;IACxE,SAAS,EAAE,IAAI,CAAc;IAC7B,OAAO,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC5B,eAAe,SAAK;IACpB,kBAAkB,SAAK;IACvB,cAAc,UAAS;IACvB,qFAAqF;IACrF,eAAe,SAAM;IACrB,eAAe,UAAS;IACxB,uGAAuG;IACvG,MAAM,SAAM;IACZ,4FAA4F;IAC5F,aAAa,SAAM;IACnB,WAAW,SAAM;IAEmC,kBAAkB,EAClE,MAAM,GACN,IAAI,CAAQ;IAChB,yFAAyF;IAC9D,QAAQ,SAAM;IAE1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAMtB,cAAc;CA6G7B;AAED,0DAA0D;AAC1D,qBAca,eAAgB,SAAQ,UAAU;IACjC,QAAQ,SAAM;IACG,OAAO,SAAM;IAC1C,SAAS,EAAE,IAAI,CAAc;IAC7B,OAAO,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC5B,IAAI,UAAS;IACb,wFAAwF;IACxF,IAAI,SAAM;IACiB,QAAQ,SAAM;IAE1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAMtB,cAAc;IAgFb,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAgBvC;AAED,QAAA,MAAM,qBAAqB,kDAAmD,CAAC;AAC/E,uDAAuD;AACvD,MAAM,MAAM,4BAA4B,GACpC,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,GACtC,EAAE,CAAC;AAEP;;;;;;GAMG;AACH,qBAsBa,iBAAkB,SAAQ,UAAU;IACnC,QAAQ,SAAM;IACG,OAAO,SAAM;IAC1C,2EAA2E;IAC3E,WAAW,SAAM;IACjB,SAAS,SAAM;IACf,6EAA6E;IAC7E,WAAW,SAAM;IACjB,SAAS,SAAM;IACf,0CAA0C;IAC1C,IAAI,SAAM;IACV,WAAW,SAAM;IACjB,SAAS,EAAE,IAAI,CAAc;IAC7B,OAAO,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC5B,0DAA0D;IAC1D,eAAe,SAAK;IACpB,4EAA4E;IAC5E,YAAY,EAAE,4BAA4B,CAAM;IAChD,iFAAiF;IACjF,cAAc,UAAS;IACI,QAAQ,SAAM;IAE1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAMtB,cAAc;IA0Fb,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAgBvC;AAED,uEAAuE;AACvE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAwBD,+EAA+E;AAC/E,qBAQa,gBAAiB,SAAQ,UAAU;IAClC,QAAQ,SAAM;IACe,SAAS,SAAM;IAC/B,OAAO,SAAM;IACb,OAAO,SAAQ;IAExC,qFAAqF;IACrF,UAAU,IAAI,uBAAuB;IAOrC,uEAAuE;IACvE,UAAU,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI;IAMjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC;AAED,gDAAgD;AAChD,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;CAC9C;AACD,4DAA4D;AAC5D,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;CAC9C;AACD,2DAA2D;AAC3D,qBAAa,2BAA4B,SAAQ,cAAc,CAAC,iBAAiB,CAAC;IAChF,MAAM,CAAC,QAAQ,CAAC,UAAU,2BAAqB;CAChD;AACD,sCAAsC;AACtC,qBAAa,0BAA2B,SAAQ,cAAc,CAAC,gBAAgB,CAAC;IAC9E,MAAM,CAAC,QAAQ,CAAC,UAAU,0BAAoB;CAC/C"}
@@ -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