iobroker.time-tracker 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +48 -18
  2. package/admin/i18n/de.json +1 -1
  3. package/admin/i18n/en.json +1 -1
  4. package/admin/i18n/es.json +1 -1
  5. package/admin/i18n/fr.json +1 -1
  6. package/admin/i18n/it.json +1 -1
  7. package/admin/i18n/nl.json +1 -1
  8. package/admin/i18n/pl.json +1 -1
  9. package/admin/i18n/pt.json +1 -1
  10. package/admin/i18n/ru.json +1 -1
  11. package/admin/i18n/uk.json +1 -1
  12. package/admin/i18n/zh-cn.json +1 -1
  13. package/admin/jsonConfig.json +1 -1
  14. package/build/lib/db/migrations.js +39 -0
  15. package/build/lib/db/migrations.js.map +2 -2
  16. package/build/lib/db/repositories/dayNotes.js +183 -0
  17. package/build/lib/db/repositories/dayNotes.js.map +7 -0
  18. package/build/lib/db/seed.js +1 -1
  19. package/build/lib/db/seed.js.map +2 -2
  20. package/build/lib/services/sync.js +11 -9
  21. package/build/lib/services/sync.js.map +2 -2
  22. package/build/lib/web/api.js +88 -12
  23. package/build/lib/web/api.js.map +2 -2
  24. package/build/lib/web/events.js.map +2 -2
  25. package/build/main.js +4 -0
  26. package/build/main.js.map +2 -2
  27. package/io-package.json +14 -14
  28. package/package.json +1 -1
  29. package/src-pwa/src/i18n/de.json +11 -1
  30. package/src-pwa/src/i18n/en.json +11 -1
  31. package/src-pwa/src/i18n/es.json +11 -1
  32. package/src-pwa/src/i18n/fr.json +11 -1
  33. package/src-pwa/src/i18n/it.json +11 -1
  34. package/src-pwa/src/i18n/nl.json +11 -1
  35. package/src-pwa/src/i18n/pl.json +11 -1
  36. package/src-pwa/src/i18n/pt.json +11 -1
  37. package/src-pwa/src/i18n/ru.json +11 -1
  38. package/src-pwa/src/i18n/uk.json +11 -1
  39. package/src-pwa/src/i18n/zh-cn.json +11 -1
  40. package/www/assets/index-D5fSC8CA.js +119 -0
  41. package/www/assets/index-D5fSC8CA.js.map +1 -0
  42. package/www/index.html +1 -1
  43. package/www/sw.js +1 -1
  44. package/www/sw.js.map +1 -1
  45. package/www/assets/index-DIM5i7zh.js +0 -119
  46. package/www/assets/index-DIM5i7zh.js.map +0 -1
@@ -510,6 +510,21 @@ function createApi(deps) {
510
510
  const raw = context.query("userId");
511
511
  return resolveScope(context, raw ? Number(raw) : null, ownPermission, otherPermission);
512
512
  };
513
+ const dayNoteTarget = (context, actorId) => {
514
+ var _a2, _b2;
515
+ const raw = context.query("userId");
516
+ const target = raw === null || raw.trim() === "" ? actorId : Number(raw);
517
+ if (!Number.isInteger(target)) {
518
+ throw new import_errors.ValidationError("userId must be a whole number");
519
+ }
520
+ if (target !== actorId && !((_b2 = (_a2 = context.auth) == null ? void 0 : _a2.permissions.includes("time.edit_other")) != null ? _b2 : false)) {
521
+ throw (0, import_problem.problem)(403, "permission_denied", "request rejected (permission_denied: time.edit_other)");
522
+ }
523
+ if (!users.findById(target)) {
524
+ throw new import_errors.NotFoundError(`user ${target} not found`);
525
+ }
526
+ return target;
527
+ };
513
528
  const numberQuery = (context, name) => {
514
529
  const raw = context.query(name);
515
530
  const value = Number(raw);
@@ -612,6 +627,9 @@ function createApi(deps) {
612
627
  const result = sync.sync({
613
628
  userId: user.id,
614
629
  timeZone: user.timezone,
630
+ // the offline queue is the only way an employee still writes a punch of his own, so the window of the
631
+ // instance applies to it as well (`0` switches the bound off)
632
+ maxPastSeconds: Math.max(0, settings.getNumber("edit_window_days", 0)) * 86400,
615
633
  punches: body.punches.map((raw) => {
616
634
  const punch2 = raw != null ? raw : {};
617
635
  const tsUtc = optionalNumber(punch2, "tsUtc");
@@ -648,10 +666,13 @@ function createApi(deps) {
648
666
  "GET",
649
667
  "/entries/conflicts",
650
668
  { permission: "time.resolve_conflict" },
651
- (context) => {
652
- var _a2, _b2;
653
- return (0, import_router.json)(200, { conflicts: sync.conflicts((_b2 = (_a2 = context.auth) == null ? void 0 : _a2.user.id) != null ? _b2 : 0) });
654
- }
669
+ (context) => (
670
+ // without `?userId=` the own queue; the administration reads the queue of an employee with it, because a
671
+ // conflict of an employee waits for a decision of the office
672
+ (0, import_router.json)(200, {
673
+ conflicts: sync.conflicts(scopeUser(context, "time.resolve_conflict", "time.resolve_conflict"))
674
+ })
675
+ )
655
676
  );
656
677
  route("POST", "/entries/:id/resolve", { permission: "time.resolve_conflict", csrf: true }, (context) => {
657
678
  var _a2, _b2;
@@ -732,7 +753,7 @@ function createApi(deps) {
732
753
  }
733
754
  return (0, import_router.json)(200, { entries: entries.listByRange(userId, from, to) });
734
755
  });
735
- route("POST", "/entries", { permission: "time.edit_own", csrf: true }, (context) => {
756
+ route("POST", "/entries", { permission: "time.edit_other", csrf: true }, (context) => {
736
757
  var _a2, _b2;
737
758
  if (!context.auth) {
738
759
  throw (0, import_problem.problem)(401, "no_session", "request rejected (no_session)");
@@ -742,10 +763,6 @@ function createApi(deps) {
742
763
  if (!Number.isInteger(target)) {
743
764
  throw new import_errors.ValidationError("userId must be a whole number");
744
765
  }
745
- const own = target === context.auth.user.id;
746
- if (!own && !context.auth.permissions.includes("time.edit_other")) {
747
- throw (0, import_problem.problem)(403, "permission_denied", "request rejected (permission_denied: time.edit_other)");
748
- }
749
766
  const user = users.findById(target);
750
767
  if (!user) {
751
768
  throw new import_errors.NotFoundError(`user ${target} not found`);
@@ -761,7 +778,8 @@ function createApi(deps) {
761
778
  tsUtc,
762
779
  clientTsUtc: optionalNumber(body, "clientTsUtc"),
763
780
  timeZone: user.timezone,
764
- source: own ? "web" : "admin",
781
+ // a hand-written punch is always a correction; the normal punch comes through `POST /punch` (`web`)
782
+ source: "admin",
765
783
  direction: optionalDirection(body),
766
784
  idempotencyKey: optionalString(body, "idempotencyKey"),
767
785
  note: optionalString(body, "note"),
@@ -778,7 +796,7 @@ function createApi(deps) {
778
796
  entryId: stored.entry.id,
779
797
  created: stored.created,
780
798
  localDate: stored.entry.localDate,
781
- source: own ? "web" : "admin"
799
+ source: "admin"
782
800
  }
783
801
  });
784
802
  return (0, import_router.json)(
@@ -1475,9 +1493,12 @@ function createApi(deps) {
1475
1493
  if (revision === null) {
1476
1494
  throw new import_errors.ValidationError("revision is required");
1477
1495
  }
1478
- requireInsideEditWindow(context, existing.userId, existing.tsUtc);
1479
1496
  const wantedTs = optionalNumber(body, "tsUtc");
1497
+ if (wantedTs !== null && !context.auth.permissions.includes("time.edit_other")) {
1498
+ throw (0, import_problem.problem)(403, "permission_denied", "request rejected (permission_denied: time.edit_other)");
1499
+ }
1480
1500
  if (wantedTs !== null) {
1501
+ requireInsideEditWindow(context, existing.userId, existing.tsUtc);
1481
1502
  requireInsideEditWindow(context, existing.userId, wantedTs);
1482
1503
  }
1483
1504
  const target = users.findById(existing.userId);
@@ -1530,6 +1551,61 @@ function createApi(deps) {
1530
1551
  });
1531
1552
  return (0, import_router.noContent)();
1532
1553
  });
1554
+ route("GET", "/day-notes", { permission: "report.view_own" }, (context) => {
1555
+ var _a2;
1556
+ const userId = scopeUser(context);
1557
+ const from = context.query("from");
1558
+ const to = (_a2 = context.query("to")) != null ? _a2 : from;
1559
+ if (!from || !to) {
1560
+ throw new import_errors.ValidationError("from and to are required");
1561
+ }
1562
+ return (0, import_router.json)(200, { notes: deps.dayNotes.listByRange(userId, from, to) });
1563
+ });
1564
+ route("PUT", "/day-notes", { permission: "time.edit_own", csrf: true }, (context) => {
1565
+ var _a2, _b2;
1566
+ if (!context.auth) {
1567
+ throw (0, import_problem.problem)(401, "no_session", "request rejected (no_session)");
1568
+ }
1569
+ const body = context.jsonBody();
1570
+ const userId = dayNoteTarget(context, context.auth.user.id);
1571
+ const localDate2 = context.query("date");
1572
+ if (!localDate2) {
1573
+ throw new import_errors.ValidationError("date is required");
1574
+ }
1575
+ const note = deps.dayNotes.save({
1576
+ userId,
1577
+ localDate: localDate2,
1578
+ // an empty text is how a note is taken back
1579
+ note: (_a2 = optionalString(body, "note")) != null ? _a2 : "",
1580
+ actorId: context.auth.user.id,
1581
+ actorIp: (_b2 = context.request.remoteAddress) != null ? _b2 : null,
1582
+ now: now()
1583
+ });
1584
+ emit({ type: "dayNote.change", userId, data: { localDate: localDate2 } });
1585
+ return (0, import_router.json)(200, { note });
1586
+ });
1587
+ route("POST", "/day-notes/handled", { permission: "time.edit_other", csrf: true }, (context) => {
1588
+ var _a2, _b2;
1589
+ if (!context.auth) {
1590
+ throw (0, import_problem.problem)(401, "no_session", "request rejected (no_session)");
1591
+ }
1592
+ const userId = dayNoteTarget(context, context.auth.user.id);
1593
+ const localDate2 = context.query("date");
1594
+ if (!localDate2) {
1595
+ throw new import_errors.ValidationError("date is required");
1596
+ }
1597
+ const handled = (_a2 = optionalBoolean(context.jsonBody(), "handled")) != null ? _a2 : true;
1598
+ const note = deps.dayNotes.setHandled({
1599
+ userId,
1600
+ localDate: localDate2,
1601
+ handled,
1602
+ actorId: context.auth.user.id,
1603
+ actorIp: (_b2 = context.request.remoteAddress) != null ? _b2 : null,
1604
+ now: now()
1605
+ });
1606
+ emit({ type: "dayNote.change", userId, data: { localDate: localDate2 } });
1607
+ return (0, import_router.json)(200, { note });
1608
+ });
1533
1609
  route("GET", "/entries/:id/audit", { permission: "audit.view" }, (context) => {
1534
1610
  const entryId = numberParam(context, "id");
1535
1611
  if (!entries.findById(entryId)) {