@kernhq/module-hr 0.2.0 → 0.3.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 (56) 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/capabilities.d.ts +1 -1
  6. package/dist/contract/capabilities.d.ts.map +1 -1
  7. package/dist/contract/capabilities.js +44 -0
  8. package/dist/contract/capabilities.js.map +1 -1
  9. package/dist/contract/events.d.ts +42 -0
  10. package/dist/contract/events.d.ts.map +1 -1
  11. package/dist/contract/events.js +42 -0
  12. package/dist/contract/events.js.map +1 -1
  13. package/dist/contract/index.d.ts +2 -0
  14. package/dist/contract/index.d.ts.map +1 -1
  15. package/dist/contract/index.js +2 -0
  16. package/dist/contract/index.js.map +1 -1
  17. package/dist/contract/leave.d.ts +168 -0
  18. package/dist/contract/leave.d.ts.map +1 -0
  19. package/dist/contract/leave.js +150 -0
  20. package/dist/contract/leave.js.map +1 -0
  21. package/dist/contract/permissions.d.ts +57 -0
  22. package/dist/contract/permissions.d.ts.map +1 -1
  23. package/dist/contract/permissions.js +69 -0
  24. package/dist/contract/permissions.js.map +1 -1
  25. package/dist/contract/router.d.ts +1446 -0
  26. package/dist/contract/router.d.ts.map +1 -1
  27. package/dist/contract/router.js +235 -0
  28. package/dist/contract/router.js.map +1 -1
  29. package/dist/server/router.d.ts +1646 -58
  30. package/dist/server/router.d.ts.map +1 -1
  31. package/dist/server/router.js +812 -2
  32. package/dist/server/router.js.map +1 -1
  33. package/dist/server/schema.d.ts +2084 -1
  34. package/dist/server/schema.d.ts.map +1 -1
  35. package/dist/server/schema.js +209 -0
  36. package/dist/server/schema.js.map +1 -1
  37. package/dist/server/services/approvals.d.ts +163 -0
  38. package/dist/server/services/approvals.d.ts.map +1 -0
  39. package/dist/server/services/approvals.js +325 -0
  40. package/dist/server/services/approvals.js.map +1 -0
  41. package/dist/server/services/ledger.d.ts +135 -0
  42. package/dist/server/services/ledger.d.ts.map +1 -0
  43. package/dist/server/services/ledger.js +178 -0
  44. package/dist/server/services/ledger.js.map +1 -0
  45. package/dist/server/services/people.d.ts +3 -3
  46. package/migrations/0002_leave.sql +237 -0
  47. package/migrations/meta/0002_snapshot.json +3009 -0
  48. package/migrations/meta/_journal.json +7 -0
  49. package/package.json +1 -1
  50. package/src/contract/approvals.ts +149 -0
  51. package/src/contract/capabilities.ts +44 -0
  52. package/src/contract/events.ts +57 -0
  53. package/src/contract/index.ts +2 -0
  54. package/src/contract/leave.ts +171 -0
  55. package/src/contract/permissions.ts +71 -0
  56. package/src/contract/router.ts +285 -0
@@ -1,5 +1,22 @@
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
+ DayPart,
12
+ LeaveBalance,
13
+ LeaveLedgerEntry,
14
+ LeaveRequest,
15
+ LeaveSimulation,
16
+ LeaveType,
17
+ LeaveUnit,
18
+ LedgerKind,
19
+ } from './leave.js'
3
20
  import {
4
21
  Calendar,
5
22
  CalendarDayKind,
@@ -568,6 +585,274 @@ export const hrContract = {
568
585
  .output(ok),
569
586
  },
570
587
 
588
+ // ---------------------------------------------------------------- leave
589
+ leave: {
590
+ types: {
591
+ list: baseContract
592
+ .route({ method: 'GET', path: '/leave/types', tags: t })
593
+ .input(ws.extend({ includeArchived: z.boolean().default(false) }))
594
+ .output(z.array(LeaveType)),
595
+ create: baseContract
596
+ .route({ method: 'POST', path: '/leave/types', tags: t })
597
+ .input(
598
+ ws.extend({
599
+ key: z.string().min(1).max(48),
600
+ name: z.string().min(1).max(120),
601
+ paid: z.boolean().default(true),
602
+ unit: LeaveUnit.default('day'),
603
+ color: z.string().max(32).nullish(),
604
+ icon: z.string().max(48).nullish(),
605
+ requiresDocumentAfterDays: z.number().int().min(1).nullish(),
606
+ countsWorkingDaysOnly: z.boolean().default(true),
607
+ allowNegative: z.boolean().default(false),
608
+ maxNegativeMinutes: z.number().int().min(0).default(0),
609
+ }),
610
+ )
611
+ .output(LeaveType),
612
+ update: baseContract
613
+ .route({ method: 'PATCH', path: '/leave/types/{leaveTypeId}', tags: t })
614
+ .input(
615
+ ws.extend({
616
+ leaveTypeId: z.uuid(),
617
+ name: z.string().min(1).max(120).optional(),
618
+ paid: z.boolean().optional(),
619
+ color: z.string().max(32).nullish(),
620
+ icon: z.string().max(48).nullish(),
621
+ requiresDocumentAfterDays: z.number().int().min(1).nullish(),
622
+ countsWorkingDaysOnly: z.boolean().optional(),
623
+ allowNegative: z.boolean().optional(),
624
+ maxNegativeMinutes: z.number().int().min(0).optional(),
625
+ order: z.number().int().optional(),
626
+ }),
627
+ )
628
+ .output(LeaveType),
629
+ archive: baseContract
630
+ .route({ method: 'DELETE', path: '/leave/types/{leaveTypeId}', tags: t })
631
+ .input(ws.extend({ leaveTypeId: z.uuid() }))
632
+ .output(ok),
633
+ },
634
+
635
+ /** Everything a person has, per type. Defaults to the caller when `personId` is omitted. */
636
+ balance: {
637
+ get: baseContract
638
+ .route({ method: 'GET', path: '/leave/balance', tags: t })
639
+ .input(ws.extend({ personId: z.uuid().optional(), periodYear: z.number().int().optional() }))
640
+ .output(z.array(LeaveBalance)),
641
+ },
642
+
643
+ /**
644
+ * The movements behind a balance.
645
+ *
646
+ * The reason the ledger is append-only: when somebody disputes a number, this is the answer —
647
+ * a list of what happened, in order, that nobody edited.
648
+ */
649
+ ledger: {
650
+ list: baseContract
651
+ .route({ method: 'GET', path: '/leave/ledger', tags: t })
652
+ .input(
653
+ ws.extend({
654
+ personId: z.uuid(),
655
+ leaveTypeId: z.uuid().optional(),
656
+ periodYear: z.number().int().optional(),
657
+ ...PageInput.shape,
658
+ }),
659
+ )
660
+ .output(page(LeaveLedgerEntry)),
661
+ },
662
+
663
+ /** A manual movement. Always carries a reason — an unexplained balance change is the thing HR gets asked about. */
664
+ adjust: baseContract
665
+ .route({ method: 'POST', path: '/leave/adjust', tags: t })
666
+ .input(
667
+ ws.extend({
668
+ personId: z.uuid(),
669
+ leaveTypeId: z.uuid(),
670
+ kind: LedgerKind.default('adjustment'),
671
+ amountMinutes: z.number().int(),
672
+ effectiveOn: IsoDate,
673
+ reason: z.string().min(1).max(500),
674
+ }),
675
+ )
676
+ .output(LeaveLedgerEntry),
677
+
678
+ requests: {
679
+ list: baseContract
680
+ .route({ method: 'GET', path: '/leave/requests', tags: t })
681
+ .input(
682
+ ws.extend({
683
+ ...PageInput.shape,
684
+ personId: z.uuid().optional(),
685
+ officeId: z.uuid().optional(),
686
+ status: z.array(LeaveRequest.shape.status).optional(),
687
+ from: IsoDate.optional(),
688
+ to: IsoDate.optional(),
689
+ }),
690
+ )
691
+ .output(page(LeaveRequest)),
692
+ get: baseContract
693
+ .route({ method: 'GET', path: '/leave/requests/{requestId}', tags: t })
694
+ .input(ws.extend({ requestId: z.uuid() }))
695
+ .output(LeaveRequest),
696
+ /** What it would cost and whether it would be refused — before anybody submits. */
697
+ simulate: baseContract
698
+ .route({ method: 'POST', path: '/leave/requests/simulate', tags: t })
699
+ .input(
700
+ ws.extend({
701
+ personId: z.uuid().optional(),
702
+ leaveTypeId: z.uuid(),
703
+ startsOn: IsoDate,
704
+ endsOn: IsoDate,
705
+ startPart: DayPart.default('full'),
706
+ endPart: DayPart.default('full'),
707
+ hours: z.number().min(0).max(24).nullish(),
708
+ }),
709
+ )
710
+ .output(LeaveSimulation),
711
+ create: baseContract
712
+ .route({ method: 'POST', path: '/leave/requests', tags: t })
713
+ .input(
714
+ ws.extend({
715
+ personId: z.uuid().optional(),
716
+ leaveTypeId: z.uuid(),
717
+ startsOn: IsoDate,
718
+ endsOn: IsoDate,
719
+ startPart: DayPart.default('full'),
720
+ endPart: DayPart.default('full'),
721
+ hours: z.number().min(0).max(24).nullish(),
722
+ reason: z.string().max(1000).nullish(),
723
+ documentFileId: z.uuid().nullish(),
724
+ /**
725
+ * Makes a retried submission safe. Two clicks on a slow connection must not book the
726
+ * same week twice and spend the balance twice.
727
+ */
728
+ idempotencyKey: z.string().min(8).max(128).optional(),
729
+ }),
730
+ )
731
+ .output(LeaveRequest),
732
+ /**
733
+ * Cancels a request. An approved one is reversed in the ledger rather than deleted, so the
734
+ * balance goes back up and the history still says what happened.
735
+ */
736
+ cancel: baseContract
737
+ .route({ method: 'POST', path: '/leave/requests/{requestId}/cancel', tags: t })
738
+ .input(ws.extend({ requestId: z.uuid(), reason: z.string().max(500).nullish() }))
739
+ .output(LeaveRequest),
740
+ },
741
+
742
+ /** Who is off, over a range — the answer a team actually looks at. */
743
+ team: {
744
+ calendar: baseContract
745
+ .route({ method: 'GET', path: '/leave/calendar', tags: t })
746
+ .input(
747
+ ws.extend({
748
+ from: IsoDate,
749
+ to: IsoDate,
750
+ officeId: z.uuid().optional(),
751
+ orgUnitId: z.uuid().optional(),
752
+ }),
753
+ )
754
+ .output(
755
+ z.array(
756
+ z.object({
757
+ personId: z.uuid(),
758
+ displayName: z.string(),
759
+ requestId: z.uuid(),
760
+ startsOn: IsoDate,
761
+ endsOn: IsoDate,
762
+ status: LeaveRequest.shape.status,
763
+ /**
764
+ * The type's name, or null when the viewer may not see it. Most companies want the
765
+ * team to know somebody is away without knowing it is sick leave.
766
+ */
767
+ leaveTypeName: z.string().nullable(),
768
+ color: z.string().nullable(),
769
+ }),
770
+ ),
771
+ ),
772
+ },
773
+ },
774
+
775
+ // ---------------------------------------------------------------- approvals
776
+ approvals: {
777
+ /** Everything waiting on the caller, across every subject type. */
778
+ inbox: baseContract
779
+ .route({ method: 'GET', path: '/approvals/inbox', tags: t })
780
+ .input(ws.extend({ ...PageInput.shape, includeDecided: z.boolean().default(false) }))
781
+ .output(page(ApprovalRequest)),
782
+ get: baseContract
783
+ .route({ method: 'GET', path: '/approvals/{requestId}', tags: t })
784
+ .input(ws.extend({ requestId: z.uuid() }))
785
+ .output(ApprovalRequest),
786
+ /**
787
+ * Approve or reject. Idempotent per approver per step — a double click is one decision, and the
788
+ * database refuses the second rather than counting it twice.
789
+ */
790
+ decide: baseContract
791
+ .route({ method: 'POST', path: '/approvals/{requestId}/decide', tags: t })
792
+ .input(
793
+ ws.extend({
794
+ requestId: z.uuid(),
795
+ decision: z.enum(['approve', 'reject']),
796
+ comment: z.string().max(1000).nullish(),
797
+ /** Deciding in somebody's place, through a delegation they created. */
798
+ onBehalfOfId: z.uuid().nullish(),
799
+ }),
800
+ )
801
+ .output(ApprovalRequest),
802
+ chains: {
803
+ list: baseContract
804
+ .route({ method: 'GET', path: '/approvals/chains', tags: t })
805
+ .input(ws.extend({ subjectType: ApprovalSubjectType.optional() }))
806
+ .output(z.array(ApprovalChain)),
807
+ create: baseContract
808
+ .route({ method: 'POST', path: '/approvals/chains', tags: t })
809
+ .input(
810
+ ws.extend({
811
+ name: z.string().min(1).max(120),
812
+ subjectType: ApprovalSubjectType,
813
+ spec: ApprovalChainSpec,
814
+ isDefault: z.boolean().default(false),
815
+ }),
816
+ )
817
+ .output(ApprovalChain),
818
+ update: baseContract
819
+ .route({ method: 'PATCH', path: '/approvals/chains/{chainId}', tags: t })
820
+ .input(
821
+ ws.extend({
822
+ chainId: z.uuid(),
823
+ name: z.string().min(1).max(120).optional(),
824
+ spec: ApprovalChainSpec.optional(),
825
+ isDefault: z.boolean().optional(),
826
+ }),
827
+ )
828
+ .output(ApprovalChain),
829
+ archive: baseContract
830
+ .route({ method: 'DELETE', path: '/approvals/chains/{chainId}', tags: t })
831
+ .input(ws.extend({ chainId: z.uuid() }))
832
+ .output(ok),
833
+ },
834
+ delegations: baseContract
835
+ .route({ method: 'GET', path: '/approvals/delegations', tags: t })
836
+ .input(ws.extend({ personId: z.uuid().optional() }))
837
+ .output(z.array(Delegation)),
838
+ delegate: baseContract
839
+ .route({ method: 'POST', path: '/approvals/delegations', tags: t })
840
+ .input(
841
+ ws.extend({
842
+ toPersonId: z.uuid(),
843
+ subjectType: ApprovalSubjectType.nullish(),
844
+ startsOn: IsoDate,
845
+ endsOn: IsoDate,
846
+ reason: z.string().max(200).nullish(),
847
+ }),
848
+ )
849
+ .output(Delegation),
850
+ revokeDelegation: baseContract
851
+ .route({ method: 'DELETE', path: '/approvals/delegations/{delegationId}', tags: t })
852
+ .input(ws.extend({ delegationId: z.uuid() }))
853
+ .output(ok),
854
+ },
855
+
571
856
  // ---------------------------------------------------------------- custom fields
572
857
  fields: {
573
858
  list: baseContract