@happyvertical/smrt-timesheets 0.57.0 → 0.57.2

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 (43) hide show
  1. package/AGENTS.md +9 -3
  2. package/README.md +34 -5
  3. package/agents/period-rollup.md +13 -0
  4. package/dist/attendance/correction-service.d.ts +50 -9
  5. package/dist/attendance/correction-service.d.ts.map +1 -1
  6. package/dist/attendance/models.d.ts +6 -0
  7. package/dist/attendance/models.d.ts.map +1 -1
  8. package/dist/attendance/segments.d.ts.map +1 -1
  9. package/dist/attendance/service.d.ts +7 -1
  10. package/dist/attendance/service.d.ts.map +1 -1
  11. package/dist/attendance.js +2 -2
  12. package/dist/chunks/{attendance-Cmk-6dTV.js → attendance-CRhYpFij.js} +192 -38
  13. package/dist/chunks/attendance-CRhYpFij.js.map +1 -0
  14. package/dist/chunks/{attendance-CfIgcl26.js → attendance-NMy9n6Yz.js} +6 -6
  15. package/dist/chunks/attendance-NMy9n6Yz.js.map +1 -0
  16. package/dist/chunks/{models-BCrk0kZQ.js → models-CSNK96yj.js} +14 -3
  17. package/dist/chunks/models-CSNK96yj.js.map +1 -0
  18. package/dist/chunks/{rollup-CII9Hl1R.js → rollup-BfJ0Rx8A.js} +15 -7
  19. package/dist/chunks/rollup-BfJ0Rx8A.js.map +1 -0
  20. package/dist/chunks/service-time-entry-D-b03hMX.js +286 -0
  21. package/dist/chunks/service-time-entry-D-b03hMX.js.map +1 -0
  22. package/dist/chunks/{time-entry-actions-Lb8OYIS8.js → time-entry-actions-CFvxmB3J.js} +2 -2
  23. package/dist/chunks/{time-entry-actions-Lb8OYIS8.js.map → time-entry-actions-CFvxmB3J.js.map} +1 -1
  24. package/dist/index.js +5 -5
  25. package/dist/manifest.json +58 -5
  26. package/dist/rollup/attendance.d.ts +2 -2
  27. package/dist/rollup/attendance.d.ts.map +1 -1
  28. package/dist/rollup/calculation.d.ts +7 -1
  29. package/dist/rollup/calculation.d.ts.map +1 -1
  30. package/dist/rollup/service.d.ts.map +1 -1
  31. package/dist/rollup/types.d.ts +10 -0
  32. package/dist/rollup/types.d.ts.map +1 -1
  33. package/dist/rollup.d.ts +1 -1
  34. package/dist/rollup.d.ts.map +1 -1
  35. package/dist/rollup.js +1 -1
  36. package/dist/smrt-knowledge.json +25 -10
  37. package/package.json +7 -7
  38. package/dist/chunks/attendance-CfIgcl26.js.map +0 -1
  39. package/dist/chunks/attendance-Cmk-6dTV.js.map +0 -1
  40. package/dist/chunks/models-BCrk0kZQ.js.map +0 -1
  41. package/dist/chunks/rollup-CII9Hl1R.js.map +0 -1
  42. package/dist/chunks/service-time-entry-DzQsRZc_.js +0 -286
  43. package/dist/chunks/service-time-entry-DzQsRZc_.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { i as ServiceTimeEntryCollection, o as parseTimesheetJson } from "./service-time-entry-DzQsRZc_.js";
2
- import { c as AttendanceSegmentCollection, d as persistAutoClose, f as persistCorrection, i as AttendancePunchCollection, l as deleteCorrection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, s as AttendanceSegment, t as AttendanceBreak, u as isCorrectionWrite } from "./models-BCrk0kZQ.js";
1
+ import { i as ServiceTimeEntryCollection, o as parseTimesheetJson } from "./service-time-entry-D-b03hMX.js";
2
+ import { c as AttendanceSegmentCollection, d as persistAutoClose, f as persistCorrection, i as AttendancePunchCollection, l as deleteCorrection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, s as AttendanceSegment, t as AttendanceBreak, u as isCorrectionWrite } from "./models-CSNK96yj.js";
3
3
  import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, isEmbeddedDatabase, isPostgresDatabase, smrt, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
4
4
  import { TenantScoped, getTenantId, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
5
5
  //#region src/attendance/correction-models.ts
@@ -199,6 +199,7 @@ async function beginSegment(tx, punch, at, work) {
199
199
  return tx.segments.create({
200
200
  tenantId: punch.tenantId,
201
201
  punchId: punch.id ?? void 0,
202
+ profileId: punch.profileId,
202
203
  workRefType: work.workRefType,
203
204
  workRefId: work.workRefId,
204
205
  taskRefType: work.taskRefType ?? "",
@@ -713,6 +714,8 @@ var AttendanceService = class {
713
714
  const deviceAt = new Date(tap.at);
714
715
  let at = deviceAt;
715
716
  let review = false;
717
+ let punchReview = true;
718
+ let offline = true;
716
719
  if (options.resolveAt) {
717
720
  const serverNow = this.date(options.now?.() ?? /* @__PURE__ */ new Date());
718
721
  const resolved = await options.resolveAt({
@@ -725,6 +728,8 @@ var AttendanceService = class {
725
728
  if (!resolved || !(resolved.at instanceof Date) || !Number.isFinite(resolved.at.getTime())) throw new Error("resolveAt must return a valid date.");
726
729
  at = resolved.at;
727
730
  review = resolved.reviewRequired === true;
731
+ punchReview = resolved.reviewRequired !== false;
732
+ offline = resolved.recordedOffline !== false;
728
733
  }
729
734
  let outcome;
730
735
  try {
@@ -737,8 +742,8 @@ var AttendanceService = class {
737
742
  replays: tx.replays
738
743
  };
739
744
  const { punch, segment } = await this.apply(inner, tap, at);
740
- punch.recordedOffline = true;
741
- punch.reviewRequired = true;
745
+ punch.recordedOffline ||= offline;
746
+ punch.reviewRequired ||= punchReview;
742
747
  await punch.save();
743
748
  if (segment && review) {
744
749
  segment.reviewRequired = true;
@@ -774,6 +779,7 @@ var AttendanceService = class {
774
779
  };
775
780
  //#endregion
776
781
  //#region src/attendance/correction-service.ts
782
+ var FOREVER = /* @__PURE__ */ new Date(864e13);
777
783
  var iso = (d) => d ? d.toISOString() : null;
778
784
  var punchState = (p) => ({
779
785
  startedAt: iso(p.startedAt),
@@ -837,11 +843,16 @@ var AttendanceCorrectionService = class {
837
843
  db;
838
844
  options;
839
845
  actor;
840
- /** Change a closed punch's start and/or end. A reviewed punch no longer needs review. */
846
+ /**
847
+ * Change a closed punch's start and/or end. A reviewed punch no longer needs
848
+ * review. An open punch may change `startedAt` only: it cannot be closed
849
+ * here, and its start cannot move past its earliest break or segment.
850
+ */
841
851
  async correctPunch(input) {
842
852
  return this.run(input, async (tx, subject, record) => {
843
- const punch = await this.closedPunch(tx, subject, input.punchId);
853
+ const punch = await this.anyPunch(tx, subject, input.punchId);
844
854
  const before = punchState(punch);
855
+ if (!punch.endedAt) return this.correctOpenStart(tx, subject, record, punch, input, before);
845
856
  const startedAt = input.startedAt === void 0 ? punch.startedAt : instant(input.startedAt, "startedAt");
846
857
  const endedAt = input.endedAt === void 0 ? punch.endedAt : instant(input.endedAt, "endedAt");
847
858
  if (endedAt <= startedAt) throw new AttendanceConflict("A punch must end after it starts.");
@@ -872,8 +883,13 @@ var AttendanceCorrectionService = class {
872
883
  return record("punch.corrected", punch, punch.id, before, punchState(punch));
873
884
  });
874
885
  }
875
- /** Record a missed punch retroactively, as an already-closed interval. */
886
+ /**
887
+ * Record a missed punch retroactively: a closed interval, or with
888
+ * `{ open: true }` one that is still running, carrying its closed breaks and
889
+ * segments.
890
+ */
876
891
  async addPunch(input) {
892
+ if (input.open === true) return this.addOpenPunch(input);
877
893
  return this.run(input, async (tx, subject, record) => {
878
894
  const startedAt = instant(input.startedAt, "startedAt");
879
895
  const endedAt = instant(input.endedAt, "endedAt");
@@ -905,13 +921,14 @@ var AttendanceCorrectionService = class {
905
921
  return record("punch.added", punch, punch.id, null, punchState(punch));
906
922
  });
907
923
  }
908
- /** Add a break to a closed punch, or change one (times and/or paid). */
924
+ /** Add a closed break to a punch, or change one (times and/or paid). */
909
925
  async saveBreak(input) {
910
926
  return this.run(input, async (tx, subject, record) => {
911
- const punch = await this.closedPunch(tx, subject, input.punchId);
927
+ const punch = await this.anyPunch(tx, subject, input.punchId);
912
928
  const children = await this.children(tx, punch);
913
929
  const existing = input.breakId ? children.breaks.find((b) => b.id === input.breakId) : void 0;
914
930
  if (input.breakId && !existing) throw new AttendanceConflict("Break not found in that punch.");
931
+ if (existing && !existing.endedAt) throw new AttendanceConflict("The open break is changed through AttendanceService.");
915
932
  if (!existing && (input.startedAt === void 0 || input.endedAt === void 0)) throw new AttendanceConflict("A new break needs startedAt and endedAt.");
916
933
  if (input.paid !== void 0 && typeof input.paid !== "boolean") throw new AttendanceConflict("paid must be boolean.");
917
934
  const next = {
@@ -925,10 +942,7 @@ var AttendanceCorrectionService = class {
925
942
  breaks: [...rest, next],
926
943
  segments: children.segments
927
944
  }));
928
- await this.guard(tx, subject, [{
929
- startsAt: punch.startedAt,
930
- endsAt: punch.endedAt
931
- }]);
945
+ await this.guard(tx, subject, [this.span(punch)]);
932
946
  const entry = await this.editableEntry(tx, punch);
933
947
  const before = existing ? breakState(existing) : null;
934
948
  let target;
@@ -956,7 +970,7 @@ var AttendanceCorrectionService = class {
956
970
  return record(existing ? "break.changed" : "break.added", punch, target.id, before, breakState(target));
957
971
  });
958
972
  }
959
- /** Remove a break from a closed punch. */
973
+ /** Remove a closed break from a punch. */
960
974
  async removeBreak(input) {
961
975
  return this.run(input, async (tx, subject, record) => {
962
976
  const [target] = await tx.breaks.list({
@@ -967,12 +981,10 @@ var AttendanceCorrectionService = class {
967
981
  limit: 1
968
982
  });
969
983
  if (!target) throw new AttendanceConflict("Break not found.");
970
- const punch = await this.closedPunch(tx, subject, target.punchId);
984
+ if (!target.endedAt) throw new AttendanceConflict("The open break is ended through AttendanceService.");
985
+ const punch = await this.anyPunch(tx, subject, target.punchId);
971
986
  const children = await this.children(tx, punch);
972
- await this.guard(tx, subject, [{
973
- startsAt: punch.startedAt,
974
- endsAt: punch.endedAt
975
- }]);
987
+ await this.guard(tx, subject, [this.span(punch)]);
976
988
  const entry = await this.editableEntry(tx, punch);
977
989
  const before = breakState(target);
978
990
  await deleteCorrection(target);
@@ -980,13 +992,14 @@ var AttendanceCorrectionService = class {
980
992
  return record("break.removed", punch, target.id, before, null);
981
993
  });
982
994
  }
983
- /** Add a work segment to a closed punch, or change one (times and/or work attribution). */
995
+ /** Add a closed work segment to a punch, or change one (times and/or work attribution). */
984
996
  async saveSegment(input) {
985
997
  return this.run(input, async (tx, subject, record) => {
986
- const punch = await this.closedPunch(tx, subject, input.punchId);
998
+ const punch = await this.anyPunch(tx, subject, input.punchId);
987
999
  const children = await this.children(tx, punch);
988
1000
  const existing = input.segmentId ? children.segments.find((s) => s.id === input.segmentId) : void 0;
989
1001
  if (input.segmentId && !existing) throw new AttendanceConflict("Work segment not found in that punch.");
1002
+ if (existing && !existing.endedAt) throw new AttendanceConflict("The open work segment is changed through AttendanceService.");
990
1003
  if (!existing && (input.startedAt === void 0 || input.endedAt === void 0 || !input.work)) throw new AttendanceConflict("A new work segment needs startedAt, endedAt and work.");
991
1004
  if (input.work) validateAttendanceWork(input.work);
992
1005
  const next = {
@@ -998,10 +1011,7 @@ var AttendanceCorrectionService = class {
998
1011
  breaks: children.breaks,
999
1012
  segments: [...rest, next]
1000
1013
  }));
1001
- await this.guard(tx, subject, [{
1002
- startsAt: punch.startedAt,
1003
- endsAt: punch.endedAt
1004
- }]);
1014
+ await this.guard(tx, subject, [this.span(punch)]);
1005
1015
  const before = existing ? segmentState(existing) : null;
1006
1016
  let target;
1007
1017
  if (existing) {
@@ -1019,6 +1029,7 @@ var AttendanceCorrectionService = class {
1019
1029
  Object.assign(target, {
1020
1030
  tenantId: subject.tenantId,
1021
1031
  punchId: punch.id ?? void 0,
1032
+ profileId: punch.profileId,
1022
1033
  workRefType: input.work.workRefType,
1023
1034
  workRefId: input.work.workRefId,
1024
1035
  taskRefType: input.work.taskRefType ?? "",
@@ -1033,7 +1044,7 @@ var AttendanceCorrectionService = class {
1033
1044
  return record(existing ? "segment.changed" : "segment.added", punch, target.id, before, segmentState(target));
1034
1045
  });
1035
1046
  }
1036
- /** Remove a work segment from a closed punch. */
1047
+ /** Remove a closed work segment from a punch. */
1037
1048
  async removeSegment(input) {
1038
1049
  return this.run(input, async (tx, subject, record) => {
1039
1050
  const [target] = await tx.segments.list({
@@ -1044,11 +1055,9 @@ var AttendanceCorrectionService = class {
1044
1055
  limit: 1
1045
1056
  });
1046
1057
  if (!target) throw new AttendanceConflict("Work segment not found.");
1047
- const punch = await this.closedPunch(tx, subject, target.punchId);
1048
- await this.guard(tx, subject, [{
1049
- startsAt: punch.startedAt,
1050
- endsAt: punch.endedAt
1051
- }]);
1058
+ if (!target.endedAt) throw new AttendanceConflict("The open work segment is stopped through AttendanceService.");
1059
+ const punch = await this.anyPunch(tx, subject, target.punchId);
1060
+ await this.guard(tx, subject, [this.span(punch)]);
1052
1061
  const before = segmentState(target);
1053
1062
  await deleteCorrection(target);
1054
1063
  return record("segment.removed", punch, target.id, before, null);
@@ -1099,6 +1108,148 @@ var AttendanceCorrectionService = class {
1099
1108
  orderBy: "created_at ASC"
1100
1109
  })).filter((row) => row.acknowledgementRequired && !row.acknowledgedAt));
1101
1110
  }
1111
+ /** The range a correction touches, for period-lock checks: an open punch runs to now. */
1112
+ span(punch) {
1113
+ const now = /* @__PURE__ */ new Date();
1114
+ return {
1115
+ startsAt: punch.startedAt,
1116
+ endsAt: punch.endedAt ?? (now > punch.startedAt ? now : punch.startedAt)
1117
+ };
1118
+ }
1119
+ async openPunchOf(tx, subject) {
1120
+ return (await tx.punches.list({
1121
+ where: {
1122
+ tenantId: subject.tenantId,
1123
+ profileId: subject.profileId,
1124
+ endedAt: null
1125
+ },
1126
+ limit: 1
1127
+ })).length > 0;
1128
+ }
1129
+ async correctOpenStart(tx, subject, record, punch, input, before) {
1130
+ if (input.endedAt !== void 0) throw new AttendanceConflict("An open punch cannot be closed by a correction; use AttendanceService.");
1131
+ if (input.startedAt === void 0) throw new AttendanceConflict("An open punch is corrected by startedAt.");
1132
+ const startedAt = instant(input.startedAt, "startedAt");
1133
+ const children = await this.children(tx, punch);
1134
+ validateLayout(this.layout(startedAt, null, children));
1135
+ for (const other of await tx.punches.list({ where: {
1136
+ tenantId: subject.tenantId,
1137
+ profileId: subject.profileId
1138
+ } })) if (other.id !== punch.id && overlapping({
1139
+ start: startedAt.getTime(),
1140
+ end: FOREVER.getTime()
1141
+ }, {
1142
+ start: other.startedAt.getTime(),
1143
+ end: other.endedAt?.getTime() ?? FOREVER.getTime()
1144
+ })) throw new AttendanceConflict("Punches must not overlap.");
1145
+ const earliest = punch.startedAt < startedAt ? punch.startedAt : startedAt;
1146
+ await this.guard(tx, subject, [{
1147
+ startsAt: earliest,
1148
+ endsAt: this.span(punch).endsAt
1149
+ }]);
1150
+ punch.startedAt = startedAt;
1151
+ punch.reviewRequired = false;
1152
+ await this.settle(tx, punch, children.breaks, null);
1153
+ return record("punch.corrected", punch, punch.id, before, punchState(punch));
1154
+ }
1155
+ async addOpenPunch(input) {
1156
+ return this.run(input, async (tx, subject, record) => {
1157
+ const startedAt = instant(input.startedAt, "startedAt");
1158
+ const meta = attendanceProvenance(input);
1159
+ if (input.endedAt !== void 0) throw new AttendanceConflict("An open punch has no endedAt.");
1160
+ if (await this.openPunchOf(tx, subject)) throw new AttendanceConflict("A punch is already open.", "punch_open");
1161
+ const breaks = (input.breaks ?? []).map((b) => ({
1162
+ startedAt: instant(b.startedAt, "startedAt"),
1163
+ endedAt: instant(b.endedAt, "endedAt"),
1164
+ paid: b.paid ?? false,
1165
+ kind: attendanceLabel(b.kind, "kind")
1166
+ }));
1167
+ for (const b of input.breaks ?? []) if (b.paid !== void 0 && typeof b.paid !== "boolean") throw new AttendanceConflict("paid must be boolean.");
1168
+ const segments = (input.segments ?? []).map((g) => {
1169
+ validateAttendanceWork(g.work);
1170
+ return {
1171
+ startedAt: instant(g.startedAt, "startedAt"),
1172
+ endedAt: instant(g.endedAt, "endedAt"),
1173
+ work: g.work
1174
+ };
1175
+ });
1176
+ validateLayout({
1177
+ startsAt: startedAt.getTime(),
1178
+ endsAt: FOREVER.getTime(),
1179
+ breaks,
1180
+ segments
1181
+ });
1182
+ for (const other of await tx.punches.list({ where: {
1183
+ tenantId: subject.tenantId,
1184
+ profileId: subject.profileId
1185
+ } })) if (overlapping({
1186
+ start: startedAt.getTime(),
1187
+ end: FOREVER.getTime()
1188
+ }, {
1189
+ start: other.startedAt.getTime(),
1190
+ end: other.endedAt?.getTime() ?? FOREVER.getTime()
1191
+ })) throw new AttendanceConflict("Punches must not overlap.");
1192
+ const now = /* @__PURE__ */ new Date();
1193
+ await this.guard(tx, subject, [{
1194
+ startsAt: startedAt,
1195
+ endsAt: now > startedAt ? now : startedAt
1196
+ }]);
1197
+ const punch = await tx.punches.create({
1198
+ tenantId: subject.tenantId,
1199
+ profileId: subject.profileId,
1200
+ startedAt,
1201
+ ...meta,
1202
+ _insertOnly: true
1203
+ });
1204
+ for (const b of breaks) {
1205
+ const row = await new AttendanceBreak({
1206
+ db: tx.db,
1207
+ id: crypto.randomUUID(),
1208
+ _skipLoad: true
1209
+ }).initialize();
1210
+ Object.assign(row, {
1211
+ tenantId: subject.tenantId,
1212
+ punchId: punch.id ?? void 0,
1213
+ ...b
1214
+ });
1215
+ await persistCorrection(row);
1216
+ }
1217
+ const segmentStates = [];
1218
+ for (const g of segments) {
1219
+ const row = await new AttendanceSegment({
1220
+ db: tx.db,
1221
+ id: crypto.randomUUID(),
1222
+ _skipLoad: true
1223
+ }).initialize();
1224
+ Object.assign(row, {
1225
+ tenantId: subject.tenantId,
1226
+ punchId: punch.id ?? void 0,
1227
+ profileId: punch.profileId,
1228
+ workRefType: g.work.workRefType,
1229
+ workRefId: g.work.workRefId,
1230
+ taskRefType: g.work.taskRefType ?? "",
1231
+ taskRefId: g.work.taskRefId ?? "",
1232
+ kind: g.work.kind ?? "",
1233
+ description: g.work.description,
1234
+ startedAt: g.startedAt,
1235
+ endedAt: g.endedAt,
1236
+ closedReason: "stop"
1237
+ });
1238
+ await persistCorrection(row);
1239
+ segmentStates.push(segmentState(row));
1240
+ }
1241
+ return record("punch.added", punch, punch.id, null, {
1242
+ ...punchState(punch),
1243
+ ...breaks.length ? { breaks: breaks.map((b) => ({
1244
+ startedAt: iso(b.startedAt),
1245
+ endedAt: iso(b.endedAt),
1246
+ paid: b.paid,
1247
+ ...b.kind ? { kind: b.kind } : {}
1248
+ })) } : {},
1249
+ ...segmentStates.length ? { segments: segmentStates } : {}
1250
+ });
1251
+ });
1252
+ }
1102
1253
  attendance(subjectProfileId) {
1103
1254
  return new AttendanceService(this.db, {
1104
1255
  tenantId: this.actor.tenantId,
@@ -1153,7 +1304,7 @@ var AttendanceCorrectionService = class {
1153
1304
  });
1154
1305
  return this.read(id);
1155
1306
  }
1156
- async closedPunch(tx, subject, punchId) {
1307
+ async anyPunch(tx, subject, punchId) {
1157
1308
  const [punch] = await tx.punches.list({
1158
1309
  where: {
1159
1310
  tenantId: subject.tenantId,
@@ -1163,7 +1314,6 @@ var AttendanceCorrectionService = class {
1163
1314
  limit: 1
1164
1315
  });
1165
1316
  if (!punch) throw new AttendanceConflict("Punch not found for subject.");
1166
- if (!punch.endedAt) throw new AttendanceConflict("Only closed punches are corrected; use AttendanceService for an open one.");
1167
1317
  return punch;
1168
1318
  }
1169
1319
  async children(tx, punch) {
@@ -1179,7 +1329,7 @@ var AttendanceCorrectionService = class {
1179
1329
  where,
1180
1330
  orderBy: "started_at ASC"
1181
1331
  });
1182
- if (breaks.some((b) => !b.endedAt) || segments.some((s) => !s.endedAt)) throw new AttendanceConflict("Closed punch has an open break or segment.");
1332
+ if (punch.endedAt && (breaks.some((b) => !b.endedAt) || segments.some((s) => !s.endedAt))) throw new AttendanceConflict("Closed punch has an open break or segment.");
1183
1333
  return {
1184
1334
  breaks,
1185
1335
  segments
@@ -1188,15 +1338,15 @@ var AttendanceCorrectionService = class {
1188
1338
  layout(startedAt, endedAt, children) {
1189
1339
  return {
1190
1340
  startsAt: startedAt.getTime(),
1191
- endsAt: endedAt.getTime(),
1341
+ endsAt: endedAt ? endedAt.getTime() : FOREVER.getTime(),
1192
1342
  breaks: children.breaks.map((b) => ({
1193
1343
  startedAt: b.startedAt,
1194
- endedAt: b.endedAt,
1344
+ endedAt: b.endedAt ?? FOREVER,
1195
1345
  paid: b.paid
1196
1346
  })),
1197
1347
  segments: children.segments.map((s) => ({
1198
1348
  startedAt: s.startedAt,
1199
- endedAt: s.endedAt
1349
+ endedAt: s.endedAt ?? FOREVER
1200
1350
  }))
1201
1351
  };
1202
1352
  }
@@ -1228,6 +1378,10 @@ var AttendanceCorrectionService = class {
1228
1378
  }
1229
1379
  /** Recompute the punch's durations from its breaks and persist it and its draft entry. */
1230
1380
  async settle(tx, punch, breaks, entry) {
1381
+ if (!punch.endedAt) {
1382
+ await persistCorrection(punch);
1383
+ return;
1384
+ }
1231
1385
  const unpaidMs = breaks.filter((b) => !b.paid).reduce((sum, b) => sum + b.endedAt.getTime() - b.startedAt.getTime(), 0);
1232
1386
  punch.unpaidBreakSeconds = Math.round(unpaidMs / 1e3);
1233
1387
  punch.durationSeconds = Math.round((punch.endedAt.getTime() - punch.startedAt.getTime() - unpaidMs) / 1e3);
@@ -1243,4 +1397,4 @@ var AttendanceCorrectionService = class {
1243
1397
  //#endregion
1244
1398
  export { continueSegment as a, openSegment as c, validateAttendanceWork as d, ATTENDANCE_CORRECTION_KINDS as f, closeOpenSegment as i, replaceSegment as l, AttendanceCorrectionCollection as m, AttendanceService as n, endSegment as o, AttendanceCorrection as p, beginSegment as r, listSegments as s, AttendanceCorrectionService as t, AttendanceConflict as u };
1245
1399
 
1246
- //# sourceMappingURL=attendance-Cmk-6dTV.js.map
1400
+ //# sourceMappingURL=attendance-CRhYpFij.js.map