@kernhq/module-hr 0.2.0 → 0.4.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 (82) hide show
  1. package/dist/contract/approvals.d.ts +281 -0
  2. package/dist/contract/approvals.d.ts.map +1 -0
  3. package/dist/contract/approvals.js +127 -0
  4. package/dist/contract/approvals.js.map +1 -0
  5. package/dist/contract/attendance.d.ts +240 -0
  6. package/dist/contract/attendance.d.ts.map +1 -0
  7. package/dist/contract/attendance.js +154 -0
  8. package/dist/contract/attendance.js.map +1 -0
  9. package/dist/contract/capabilities.d.ts +1 -1
  10. package/dist/contract/capabilities.d.ts.map +1 -1
  11. package/dist/contract/capabilities.js +80 -0
  12. package/dist/contract/capabilities.js.map +1 -1
  13. package/dist/contract/events.d.ts +60 -0
  14. package/dist/contract/events.d.ts.map +1 -1
  15. package/dist/contract/events.js +60 -0
  16. package/dist/contract/events.js.map +1 -1
  17. package/dist/contract/index.d.ts +3 -0
  18. package/dist/contract/index.d.ts.map +1 -1
  19. package/dist/contract/index.js +3 -0
  20. package/dist/contract/index.js.map +1 -1
  21. package/dist/contract/leave.d.ts +168 -0
  22. package/dist/contract/leave.d.ts.map +1 -0
  23. package/dist/contract/leave.js +150 -0
  24. package/dist/contract/leave.js.map +1 -0
  25. package/dist/contract/permissions.d.ts +100 -0
  26. package/dist/contract/permissions.d.ts.map +1 -1
  27. package/dist/contract/permissions.js +119 -0
  28. package/dist/contract/permissions.js.map +1 -1
  29. package/dist/contract/router.d.ts +2440 -0
  30. package/dist/contract/router.d.ts.map +1 -1
  31. package/dist/contract/router.js +393 -0
  32. package/dist/contract/router.js.map +1 -1
  33. package/dist/policy/time.d.ts +48 -0
  34. package/dist/policy/time.d.ts.map +1 -0
  35. package/dist/policy/time.js +144 -0
  36. package/dist/policy/time.js.map +1 -0
  37. package/dist/policy/working-time.d.ts +98 -0
  38. package/dist/policy/working-time.d.ts.map +1 -0
  39. package/dist/policy/working-time.js +190 -0
  40. package/dist/policy/working-time.js.map +1 -0
  41. package/dist/server/index.d.ts.map +1 -1
  42. package/dist/server/index.js +2 -0
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/jobs.d.ts +16 -0
  45. package/dist/server/jobs.d.ts.map +1 -0
  46. package/dist/server/jobs.js +166 -0
  47. package/dist/server/jobs.js.map +1 -0
  48. package/dist/server/router.d.ts +2731 -58
  49. package/dist/server/router.d.ts.map +1 -1
  50. package/dist/server/router.js +1306 -3
  51. package/dist/server/router.js.map +1 -1
  52. package/dist/server/schema.d.ts +3344 -1
  53. package/dist/server/schema.d.ts.map +1 -1
  54. package/dist/server/schema.js +325 -0
  55. package/dist/server/schema.js.map +1 -1
  56. package/dist/server/services/approvals.d.ts +163 -0
  57. package/dist/server/services/approvals.d.ts.map +1 -0
  58. package/dist/server/services/approvals.js +325 -0
  59. package/dist/server/services/approvals.js.map +1 -0
  60. package/dist/server/services/attendance.d.ts +150 -0
  61. package/dist/server/services/attendance.d.ts.map +1 -0
  62. package/dist/server/services/attendance.js +239 -0
  63. package/dist/server/services/attendance.js.map +1 -0
  64. package/dist/server/services/ledger.d.ts +135 -0
  65. package/dist/server/services/ledger.d.ts.map +1 -0
  66. package/dist/server/services/ledger.js +178 -0
  67. package/dist/server/services/ledger.js.map +1 -0
  68. package/dist/server/services/people.d.ts +3 -3
  69. package/migrations/0002_leave.sql +237 -0
  70. package/migrations/0003_attendance.sql +206 -0
  71. package/migrations/meta/0002_snapshot.json +3009 -0
  72. package/migrations/meta/0003_snapshot.json +3681 -0
  73. package/migrations/meta/_journal.json +14 -0
  74. package/package.json +1 -1
  75. package/src/contract/approvals.ts +149 -0
  76. package/src/contract/attendance.ts +180 -0
  77. package/src/contract/capabilities.ts +80 -0
  78. package/src/contract/events.ts +81 -0
  79. package/src/contract/index.ts +3 -0
  80. package/src/contract/leave.ts +171 -0
  81. package/src/contract/permissions.ts +122 -0
  82. package/src/contract/router.ts +481 -0
@@ -1,5 +1,33 @@
1
1
  import { baseContract, PageInput, page, WorkspaceId } from '@kernhq/contracts'
2
2
  import { z } from 'zod'
3
+ import {
4
+ ApprovalChain,
5
+ ApprovalChainSpec,
6
+ ApprovalRequest,
7
+ ApprovalSubjectType,
8
+ Delegation,
9
+ } from './approvals.js'
10
+ import {
11
+ AttendanceDay,
12
+ ClockState,
13
+ Punch,
14
+ PunchDirection,
15
+ PunchMethod,
16
+ Regularization,
17
+ Schedule,
18
+ ScheduleAssignment,
19
+ ScheduleWeek,
20
+ } from './attendance.js'
21
+ import {
22
+ DayPart,
23
+ LeaveBalance,
24
+ LeaveLedgerEntry,
25
+ LeaveRequest,
26
+ LeaveSimulation,
27
+ LeaveType,
28
+ LeaveUnit,
29
+ LedgerKind,
30
+ } from './leave.js'
3
31
  import {
4
32
  Calendar,
5
33
  CalendarDayKind,
@@ -568,6 +596,459 @@ export const hrContract = {
568
596
  .output(ok),
569
597
  },
570
598
 
599
+ // ---------------------------------------------------------------- attendance
600
+ attendance: {
601
+ /** Am I clocked in? The one call a clock widget makes. */
602
+ state: baseContract
603
+ .route({ method: 'GET', path: '/attendance/state', tags: t })
604
+ .input(ws.extend({ personId: z.uuid().optional() }))
605
+ .output(ClockState),
606
+
607
+ /**
608
+ * Clock in. The instant is **stamped by the server**, never taken from the caller.
609
+ *
610
+ * `clientReportedAt` is recorded alongside it for audit — a device whose clock is an hour out is
611
+ * worth knowing about — but it never decides anything.
612
+ */
613
+ clockIn: baseContract
614
+ .route({ method: 'POST', path: '/attendance/clock-in', tags: t })
615
+ .input(
616
+ ws.extend({
617
+ personId: z.uuid().optional(),
618
+ method: PunchMethod.default('web'),
619
+ clientReportedAt: z.iso.datetime({ offset: true }).nullish(),
620
+ geo: z.object({ lat: z.number(), lng: z.number(), accuracyM: z.number().optional() }).nullish(),
621
+ note: z.string().max(500).nullish(),
622
+ idempotencyKey: z.string().min(8).max(128).optional(),
623
+ }),
624
+ )
625
+ .output(Punch),
626
+ clockOut: baseContract
627
+ .route({ method: 'POST', path: '/attendance/clock-out', tags: t })
628
+ .input(
629
+ ws.extend({
630
+ personId: z.uuid().optional(),
631
+ method: PunchMethod.default('web'),
632
+ clientReportedAt: z.iso.datetime({ offset: true }).nullish(),
633
+ geo: z.object({ lat: z.number(), lng: z.number(), accuracyM: z.number().optional() }).nullish(),
634
+ note: z.string().max(500).nullish(),
635
+ idempotencyKey: z.string().min(8).max(128).optional(),
636
+ }),
637
+ )
638
+ .output(Punch),
639
+ breakStart: baseContract
640
+ .route({ method: 'POST', path: '/attendance/break-start', tags: t })
641
+ .input(
642
+ ws.extend({ personId: z.uuid().optional(), idempotencyKey: z.string().min(8).max(128).optional() }),
643
+ )
644
+ .output(Punch),
645
+ breakEnd: baseContract
646
+ .route({ method: 'POST', path: '/attendance/break-end', tags: t })
647
+ .input(
648
+ ws.extend({ personId: z.uuid().optional(), idempotencyKey: z.string().min(8).max(128).optional() }),
649
+ )
650
+ .output(Punch),
651
+
652
+ punches: {
653
+ list: baseContract
654
+ .route({ method: 'GET', path: '/attendance/punches', tags: t })
655
+ .input(
656
+ ws.extend({
657
+ personId: z.uuid().optional(),
658
+ from: IsoDate,
659
+ to: IsoDate,
660
+ includeVoided: z.boolean().default(false),
661
+ ...PageInput.shape,
662
+ }),
663
+ )
664
+ .output(page(Punch)),
665
+ /**
666
+ * Void a punch by writing a correcting row.
667
+ *
668
+ * The original is never edited or deleted — an attendance record somebody can quietly rewrite
669
+ * is worth nothing in the dispute it exists for.
670
+ */
671
+ void: baseContract
672
+ .route({ method: 'POST', path: '/attendance/punches/{punchId}/void', tags: t })
673
+ .input(ws.extend({ punchId: z.uuid(), reason: z.string().min(1).max(500) }))
674
+ .output(ok),
675
+ },
676
+
677
+ days: {
678
+ list: baseContract
679
+ .route({ method: 'GET', path: '/attendance/days', tags: t })
680
+ .input(
681
+ ws.extend({
682
+ personId: z.uuid().optional(),
683
+ officeId: z.uuid().optional(),
684
+ from: IsoDate,
685
+ to: IsoDate,
686
+ ...PageInput.shape,
687
+ }),
688
+ )
689
+ .output(page(AttendanceDay)),
690
+ /**
691
+ * Recompute a range from the punches.
692
+ *
693
+ * Safe to call at any time — the day sheet is a projection, so this is idempotent by
694
+ * construction. Locked days are skipped and named in the response rather than silently
695
+ * ignored.
696
+ */
697
+ recompute: baseContract
698
+ .route({ method: 'POST', path: '/attendance/days/recompute', tags: t })
699
+ .input(ws.extend({ personId: z.uuid().optional(), from: IsoDate, to: IsoDate }))
700
+ .output(z.object({ recomputed: z.number().int(), skippedLocked: z.array(IsoDate) })),
701
+ },
702
+
703
+ schedules: {
704
+ list: baseContract
705
+ .route({ method: 'GET', path: '/attendance/schedules', tags: t })
706
+ .input(ws.extend({ includeArchived: z.boolean().default(false) }))
707
+ .output(z.array(Schedule)),
708
+ create: baseContract
709
+ .route({ method: 'POST', path: '/attendance/schedules', tags: t })
710
+ .input(
711
+ ws.extend({
712
+ name: z.string().min(1).max(120),
713
+ kind: Schedule.shape.kind.default('fixed'),
714
+ week: ScheduleWeek,
715
+ tzMode: Schedule.shape.tzMode.default('office'),
716
+ tz: Schedule.shape.tz.optional(),
717
+ graceInMinutes: z.number().int().min(0).max(240).default(0),
718
+ graceOutMinutes: z.number().int().min(0).max(240).default(0),
719
+ roundingStepMinutes: z.number().int().min(0).max(60).default(0),
720
+ roundingDirection: Schedule.shape.roundingDirection.default('nearest'),
721
+ autoClockOutAfterMinutes: z.number().int().min(60).nullish(),
722
+ }),
723
+ )
724
+ .output(Schedule),
725
+ update: baseContract
726
+ .route({ method: 'PATCH', path: '/attendance/schedules/{scheduleId}', tags: t })
727
+ .input(
728
+ ws.extend({
729
+ scheduleId: z.uuid(),
730
+ name: z.string().min(1).max(120).optional(),
731
+ week: ScheduleWeek.optional(),
732
+ graceInMinutes: z.number().int().min(0).max(240).optional(),
733
+ graceOutMinutes: z.number().int().min(0).max(240).optional(),
734
+ roundingStepMinutes: z.number().int().min(0).max(60).optional(),
735
+ roundingDirection: Schedule.shape.roundingDirection.optional(),
736
+ autoClockOutAfterMinutes: z.number().int().min(60).nullish(),
737
+ }),
738
+ )
739
+ .output(Schedule),
740
+ archive: baseContract
741
+ .route({ method: 'DELETE', path: '/attendance/schedules/{scheduleId}', tags: t })
742
+ .input(ws.extend({ scheduleId: z.uuid() }))
743
+ .output(ok),
744
+ assign: baseContract
745
+ .route({ method: 'POST', path: '/attendance/schedules/{scheduleId}/assign', tags: t })
746
+ .input(ws.extend({ scheduleId: z.uuid(), personId: z.uuid(), effectiveFrom: IsoDate }))
747
+ .output(z.array(ScheduleAssignment)),
748
+ },
749
+
750
+ regularizations: {
751
+ list: baseContract
752
+ .route({ method: 'GET', path: '/attendance/regularizations', tags: t })
753
+ .input(
754
+ ws.extend({
755
+ personId: z.uuid().optional(),
756
+ status: z.array(Regularization.shape.status).optional(),
757
+ ...PageInput.shape,
758
+ }),
759
+ )
760
+ .output(page(Regularization)),
761
+ /** Ask for a wrong or missing punch to be fixed. Goes through the same approval engine. */
762
+ request: baseContract
763
+ .route({ method: 'POST', path: '/attendance/regularizations', tags: t })
764
+ .input(
765
+ ws.extend({
766
+ personId: z.uuid().optional(),
767
+ businessDate: IsoDate,
768
+ punchId: z.uuid().nullish(),
769
+ proposed: z
770
+ .array(
771
+ z.object({
772
+ direction: PunchDirection,
773
+ at: z.iso.datetime({ offset: true }),
774
+ }),
775
+ )
776
+ .min(1),
777
+ reason: z.string().min(1).max(1000),
778
+ }),
779
+ )
780
+ .output(Regularization),
781
+ },
782
+ },
783
+
784
+ // ---------------------------------------------------------------- leave
785
+ leave: {
786
+ types: {
787
+ list: baseContract
788
+ .route({ method: 'GET', path: '/leave/types', tags: t })
789
+ .input(ws.extend({ includeArchived: z.boolean().default(false) }))
790
+ .output(z.array(LeaveType)),
791
+ create: baseContract
792
+ .route({ method: 'POST', path: '/leave/types', tags: t })
793
+ .input(
794
+ ws.extend({
795
+ key: z.string().min(1).max(48),
796
+ name: z.string().min(1).max(120),
797
+ paid: z.boolean().default(true),
798
+ unit: LeaveUnit.default('day'),
799
+ color: z.string().max(32).nullish(),
800
+ icon: z.string().max(48).nullish(),
801
+ requiresDocumentAfterDays: z.number().int().min(1).nullish(),
802
+ countsWorkingDaysOnly: z.boolean().default(true),
803
+ allowNegative: z.boolean().default(false),
804
+ maxNegativeMinutes: z.number().int().min(0).default(0),
805
+ }),
806
+ )
807
+ .output(LeaveType),
808
+ update: baseContract
809
+ .route({ method: 'PATCH', path: '/leave/types/{leaveTypeId}', tags: t })
810
+ .input(
811
+ ws.extend({
812
+ leaveTypeId: z.uuid(),
813
+ name: z.string().min(1).max(120).optional(),
814
+ paid: z.boolean().optional(),
815
+ color: z.string().max(32).nullish(),
816
+ icon: z.string().max(48).nullish(),
817
+ requiresDocumentAfterDays: z.number().int().min(1).nullish(),
818
+ countsWorkingDaysOnly: z.boolean().optional(),
819
+ allowNegative: z.boolean().optional(),
820
+ maxNegativeMinutes: z.number().int().min(0).optional(),
821
+ order: z.number().int().optional(),
822
+ }),
823
+ )
824
+ .output(LeaveType),
825
+ archive: baseContract
826
+ .route({ method: 'DELETE', path: '/leave/types/{leaveTypeId}', tags: t })
827
+ .input(ws.extend({ leaveTypeId: z.uuid() }))
828
+ .output(ok),
829
+ },
830
+
831
+ /** Everything a person has, per type. Defaults to the caller when `personId` is omitted. */
832
+ balance: {
833
+ get: baseContract
834
+ .route({ method: 'GET', path: '/leave/balance', tags: t })
835
+ .input(ws.extend({ personId: z.uuid().optional(), periodYear: z.number().int().optional() }))
836
+ .output(z.array(LeaveBalance)),
837
+ },
838
+
839
+ /**
840
+ * The movements behind a balance.
841
+ *
842
+ * The reason the ledger is append-only: when somebody disputes a number, this is the answer —
843
+ * a list of what happened, in order, that nobody edited.
844
+ */
845
+ ledger: {
846
+ list: baseContract
847
+ .route({ method: 'GET', path: '/leave/ledger', tags: t })
848
+ .input(
849
+ ws.extend({
850
+ personId: z.uuid(),
851
+ leaveTypeId: z.uuid().optional(),
852
+ periodYear: z.number().int().optional(),
853
+ ...PageInput.shape,
854
+ }),
855
+ )
856
+ .output(page(LeaveLedgerEntry)),
857
+ },
858
+
859
+ /** A manual movement. Always carries a reason — an unexplained balance change is the thing HR gets asked about. */
860
+ adjust: baseContract
861
+ .route({ method: 'POST', path: '/leave/adjust', tags: t })
862
+ .input(
863
+ ws.extend({
864
+ personId: z.uuid(),
865
+ leaveTypeId: z.uuid(),
866
+ kind: LedgerKind.default('adjustment'),
867
+ amountMinutes: z.number().int(),
868
+ effectiveOn: IsoDate,
869
+ reason: z.string().min(1).max(500),
870
+ }),
871
+ )
872
+ .output(LeaveLedgerEntry),
873
+
874
+ requests: {
875
+ list: baseContract
876
+ .route({ method: 'GET', path: '/leave/requests', tags: t })
877
+ .input(
878
+ ws.extend({
879
+ ...PageInput.shape,
880
+ personId: z.uuid().optional(),
881
+ officeId: z.uuid().optional(),
882
+ status: z.array(LeaveRequest.shape.status).optional(),
883
+ from: IsoDate.optional(),
884
+ to: IsoDate.optional(),
885
+ }),
886
+ )
887
+ .output(page(LeaveRequest)),
888
+ get: baseContract
889
+ .route({ method: 'GET', path: '/leave/requests/{requestId}', tags: t })
890
+ .input(ws.extend({ requestId: z.uuid() }))
891
+ .output(LeaveRequest),
892
+ /** What it would cost and whether it would be refused — before anybody submits. */
893
+ simulate: baseContract
894
+ .route({ method: 'POST', path: '/leave/requests/simulate', tags: t })
895
+ .input(
896
+ ws.extend({
897
+ personId: z.uuid().optional(),
898
+ leaveTypeId: z.uuid(),
899
+ startsOn: IsoDate,
900
+ endsOn: IsoDate,
901
+ startPart: DayPart.default('full'),
902
+ endPart: DayPart.default('full'),
903
+ hours: z.number().min(0).max(24).nullish(),
904
+ }),
905
+ )
906
+ .output(LeaveSimulation),
907
+ create: baseContract
908
+ .route({ method: 'POST', path: '/leave/requests', tags: t })
909
+ .input(
910
+ ws.extend({
911
+ personId: z.uuid().optional(),
912
+ leaveTypeId: z.uuid(),
913
+ startsOn: IsoDate,
914
+ endsOn: IsoDate,
915
+ startPart: DayPart.default('full'),
916
+ endPart: DayPart.default('full'),
917
+ hours: z.number().min(0).max(24).nullish(),
918
+ reason: z.string().max(1000).nullish(),
919
+ documentFileId: z.uuid().nullish(),
920
+ /**
921
+ * Makes a retried submission safe. Two clicks on a slow connection must not book the
922
+ * same week twice and spend the balance twice.
923
+ */
924
+ idempotencyKey: z.string().min(8).max(128).optional(),
925
+ }),
926
+ )
927
+ .output(LeaveRequest),
928
+ /**
929
+ * Cancels a request. An approved one is reversed in the ledger rather than deleted, so the
930
+ * balance goes back up and the history still says what happened.
931
+ */
932
+ cancel: baseContract
933
+ .route({ method: 'POST', path: '/leave/requests/{requestId}/cancel', tags: t })
934
+ .input(ws.extend({ requestId: z.uuid(), reason: z.string().max(500).nullish() }))
935
+ .output(LeaveRequest),
936
+ },
937
+
938
+ /** Who is off, over a range — the answer a team actually looks at. */
939
+ team: {
940
+ calendar: baseContract
941
+ .route({ method: 'GET', path: '/leave/calendar', tags: t })
942
+ .input(
943
+ ws.extend({
944
+ from: IsoDate,
945
+ to: IsoDate,
946
+ officeId: z.uuid().optional(),
947
+ orgUnitId: z.uuid().optional(),
948
+ }),
949
+ )
950
+ .output(
951
+ z.array(
952
+ z.object({
953
+ personId: z.uuid(),
954
+ displayName: z.string(),
955
+ requestId: z.uuid(),
956
+ startsOn: IsoDate,
957
+ endsOn: IsoDate,
958
+ status: LeaveRequest.shape.status,
959
+ /**
960
+ * The type's name, or null when the viewer may not see it. Most companies want the
961
+ * team to know somebody is away without knowing it is sick leave.
962
+ */
963
+ leaveTypeName: z.string().nullable(),
964
+ color: z.string().nullable(),
965
+ }),
966
+ ),
967
+ ),
968
+ },
969
+ },
970
+
971
+ // ---------------------------------------------------------------- approvals
972
+ approvals: {
973
+ /** Everything waiting on the caller, across every subject type. */
974
+ inbox: baseContract
975
+ .route({ method: 'GET', path: '/approvals/inbox', tags: t })
976
+ .input(ws.extend({ ...PageInput.shape, includeDecided: z.boolean().default(false) }))
977
+ .output(page(ApprovalRequest)),
978
+ get: baseContract
979
+ .route({ method: 'GET', path: '/approvals/{requestId}', tags: t })
980
+ .input(ws.extend({ requestId: z.uuid() }))
981
+ .output(ApprovalRequest),
982
+ /**
983
+ * Approve or reject. Idempotent per approver per step — a double click is one decision, and the
984
+ * database refuses the second rather than counting it twice.
985
+ */
986
+ decide: baseContract
987
+ .route({ method: 'POST', path: '/approvals/{requestId}/decide', tags: t })
988
+ .input(
989
+ ws.extend({
990
+ requestId: z.uuid(),
991
+ decision: z.enum(['approve', 'reject']),
992
+ comment: z.string().max(1000).nullish(),
993
+ /** Deciding in somebody's place, through a delegation they created. */
994
+ onBehalfOfId: z.uuid().nullish(),
995
+ }),
996
+ )
997
+ .output(ApprovalRequest),
998
+ chains: {
999
+ list: baseContract
1000
+ .route({ method: 'GET', path: '/approvals/chains', tags: t })
1001
+ .input(ws.extend({ subjectType: ApprovalSubjectType.optional() }))
1002
+ .output(z.array(ApprovalChain)),
1003
+ create: baseContract
1004
+ .route({ method: 'POST', path: '/approvals/chains', tags: t })
1005
+ .input(
1006
+ ws.extend({
1007
+ name: z.string().min(1).max(120),
1008
+ subjectType: ApprovalSubjectType,
1009
+ spec: ApprovalChainSpec,
1010
+ isDefault: z.boolean().default(false),
1011
+ }),
1012
+ )
1013
+ .output(ApprovalChain),
1014
+ update: baseContract
1015
+ .route({ method: 'PATCH', path: '/approvals/chains/{chainId}', tags: t })
1016
+ .input(
1017
+ ws.extend({
1018
+ chainId: z.uuid(),
1019
+ name: z.string().min(1).max(120).optional(),
1020
+ spec: ApprovalChainSpec.optional(),
1021
+ isDefault: z.boolean().optional(),
1022
+ }),
1023
+ )
1024
+ .output(ApprovalChain),
1025
+ archive: baseContract
1026
+ .route({ method: 'DELETE', path: '/approvals/chains/{chainId}', tags: t })
1027
+ .input(ws.extend({ chainId: z.uuid() }))
1028
+ .output(ok),
1029
+ },
1030
+ delegations: baseContract
1031
+ .route({ method: 'GET', path: '/approvals/delegations', tags: t })
1032
+ .input(ws.extend({ personId: z.uuid().optional() }))
1033
+ .output(z.array(Delegation)),
1034
+ delegate: baseContract
1035
+ .route({ method: 'POST', path: '/approvals/delegations', tags: t })
1036
+ .input(
1037
+ ws.extend({
1038
+ toPersonId: z.uuid(),
1039
+ subjectType: ApprovalSubjectType.nullish(),
1040
+ startsOn: IsoDate,
1041
+ endsOn: IsoDate,
1042
+ reason: z.string().max(200).nullish(),
1043
+ }),
1044
+ )
1045
+ .output(Delegation),
1046
+ revokeDelegation: baseContract
1047
+ .route({ method: 'DELETE', path: '/approvals/delegations/{delegationId}', tags: t })
1048
+ .input(ws.extend({ delegationId: z.uuid() }))
1049
+ .output(ok),
1050
+ },
1051
+
571
1052
  // ---------------------------------------------------------------- custom fields
572
1053
  fields: {
573
1054
  list: baseContract