@burdenoff/microfe-crewfoundry 2026.1013.2 โ†’ 2026.1029.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 (198) hide show
  1. package/dist/crewfoundry/CrewFoundryRoot.d.ts.map +1 -1
  2. package/dist/crewfoundry/CrewFoundryRoot.js +160 -256
  3. package/dist/crewfoundry/CrewFoundryRoot.js.map +1 -1
  4. package/dist/crewfoundry/CrewFoundryRoutes.d.ts.map +1 -1
  5. package/dist/crewfoundry/CrewFoundryRoutes.js +152 -192
  6. package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
  7. package/dist/crewfoundry/components/InlineError.d.ts +5 -0
  8. package/dist/crewfoundry/components/InlineError.d.ts.map +1 -0
  9. package/dist/crewfoundry/components/InlineError.js +17 -0
  10. package/dist/crewfoundry/components/InlineError.js.map +1 -0
  11. package/dist/crewfoundry/components/marketplace/BookServiceModal.d.ts +14 -0
  12. package/dist/crewfoundry/components/marketplace/BookServiceModal.d.ts.map +1 -0
  13. package/dist/crewfoundry/components/marketplace/BookServiceModal.js +75 -0
  14. package/dist/crewfoundry/components/marketplace/BookServiceModal.js.map +1 -0
  15. package/dist/crewfoundry/components/marketplace/MarketplaceTabs.d.ts +24 -0
  16. package/dist/crewfoundry/components/marketplace/MarketplaceTabs.d.ts.map +1 -0
  17. package/dist/crewfoundry/components/marketplace/MarketplaceTabs.js +47 -0
  18. package/dist/crewfoundry/components/marketplace/MarketplaceTabs.js.map +1 -0
  19. package/dist/crewfoundry/components/marketplace/ServiceListingModal.d.ts +12 -0
  20. package/dist/crewfoundry/components/marketplace/ServiceListingModal.d.ts.map +1 -0
  21. package/dist/crewfoundry/components/marketplace/ServiceListingModal.js +123 -0
  22. package/dist/crewfoundry/components/marketplace/ServiceListingModal.js.map +1 -0
  23. package/dist/crewfoundry/components/viz/CertificateCard.js +4 -118
  24. package/dist/crewfoundry/components/viz/PathSteps.js +3 -68
  25. package/dist/crewfoundry/components/viz/lib/geometry.js +5 -13
  26. package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -1
  27. package/dist/crewfoundry/lib/analytics.d.ts +1 -6
  28. package/dist/crewfoundry/lib/analytics.d.ts.map +1 -1
  29. package/dist/crewfoundry/lib/analytics.js +3 -18
  30. package/dist/crewfoundry/lib/analytics.js.map +1 -1
  31. package/dist/crewfoundry/lib/companyFinance.d.ts +3 -7
  32. package/dist/crewfoundry/lib/companyFinance.d.ts.map +1 -1
  33. package/dist/crewfoundry/lib/companyFinance.js +3 -8
  34. package/dist/crewfoundry/lib/companyFinance.js.map +1 -1
  35. package/dist/crewfoundry/lib/learning.d.ts +12 -0
  36. package/dist/crewfoundry/lib/learning.d.ts.map +1 -0
  37. package/dist/crewfoundry/lib/learning.js +26 -0
  38. package/dist/crewfoundry/lib/learning.js.map +1 -0
  39. package/dist/crewfoundry/lib/marketplace.d.ts +30 -0
  40. package/dist/crewfoundry/lib/marketplace.d.ts.map +1 -0
  41. package/dist/crewfoundry/lib/marketplace.js +91 -0
  42. package/dist/crewfoundry/lib/marketplace.js.map +1 -0
  43. package/dist/crewfoundry/lib/useEnsureLoaded.d.ts +7 -0
  44. package/dist/crewfoundry/lib/useEnsureLoaded.d.ts.map +1 -0
  45. package/dist/crewfoundry/lib/useEnsureLoaded.js +25 -0
  46. package/dist/crewfoundry/lib/useEnsureLoaded.js.map +1 -0
  47. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.d.ts.map +1 -1
  48. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js +12 -25
  49. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
  50. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.d.ts.map +1 -1
  51. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +145 -157
  52. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
  53. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.d.ts.map +1 -1
  54. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +14 -14
  55. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  56. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.d.ts.map +1 -1
  57. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +14 -14
  58. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
  59. package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.d.ts.map +1 -1
  60. package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.js +71 -33
  61. package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.js.map +1 -1
  62. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.d.ts.map +1 -1
  63. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +247 -173
  64. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
  65. package/dist/crewfoundry/pages/LearningCoursesPagePage.d.ts +12 -0
  66. package/dist/crewfoundry/pages/LearningCoursesPagePage.d.ts.map +1 -1
  67. package/dist/crewfoundry/pages/LearningCoursesPagePage.js +307 -293
  68. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  69. package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.d.ts +2 -0
  70. package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.d.ts.map +1 -0
  71. package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.js +223 -0
  72. package/dist/crewfoundry/pages/LearningEnrollmentsPagePage.js.map +1 -0
  73. package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.d.ts +4 -0
  74. package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.d.ts.map +1 -1
  75. package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.js +115 -160
  76. package/dist/crewfoundry/pages/LearningLearningAnalyticsPagePage.js.map +1 -1
  77. package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.d.ts.map +1 -1
  78. package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.js +165 -75
  79. package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.js.map +1 -1
  80. package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.d.ts +7 -0
  81. package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.d.ts.map +1 -0
  82. package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.js +175 -0
  83. package/dist/crewfoundry/pages/MarketplaceEscrowPagePage.js.map +1 -0
  84. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.d.ts.map +1 -1
  85. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +182 -223
  86. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  87. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.d.ts.map +1 -1
  88. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js +168 -227
  89. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
  90. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.d.ts.map +1 -1
  91. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +127 -290
  92. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  93. package/dist/crewfoundry/store/crewfoundryStore.d.ts +138 -78
  94. package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
  95. package/dist/crewfoundry/store/crewfoundryStore.js +323 -244
  96. package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
  97. package/dist/crewfoundry/utils/dateUtils.d.ts +5 -0
  98. package/dist/crewfoundry/utils/dateUtils.d.ts.map +1 -1
  99. package/dist/crewfoundry/utils/dateUtils.js +10 -1
  100. package/dist/crewfoundry/utils/dateUtils.js.map +1 -1
  101. package/dist/generated/global-types.d.ts +5145 -1627
  102. package/dist/generated/global-types.d.ts.map +1 -1
  103. package/dist/generated/wspace-types.d.ts +100373 -34602
  104. package/dist/generated/wspace-types.d.ts.map +1 -1
  105. package/dist/utils/pageContext.d.ts.map +1 -1
  106. package/dist/utils/pageContext.js +0 -1
  107. package/dist/utils/pageContext.js.map +1 -1
  108. package/package.json +1 -1
  109. package/dist/crewfoundry/components/viz/CertificateCard.js.map +0 -1
  110. package/dist/crewfoundry/components/viz/PathSteps.js.map +0 -1
  111. package/dist/crewfoundry/pages/CommunicationsCallsPagePage.d.ts +0 -2
  112. package/dist/crewfoundry/pages/CommunicationsCallsPagePage.d.ts.map +0 -1
  113. package/dist/crewfoundry/pages/CommunicationsCallsPagePage.js +0 -53
  114. package/dist/crewfoundry/pages/CommunicationsCallsPagePage.js.map +0 -1
  115. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.d.ts +0 -2
  116. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.d.ts.map +0 -1
  117. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +0 -107
  118. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +0 -1
  119. package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.d.ts +0 -2
  120. package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.d.ts.map +0 -1
  121. package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.js +0 -62
  122. package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.js.map +0 -1
  123. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.d.ts +0 -2
  124. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.d.ts.map +0 -1
  125. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js +0 -37
  126. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +0 -1
  127. package/dist/crewfoundry/pages/LearningCertificatesPagePage.d.ts +0 -2
  128. package/dist/crewfoundry/pages/LearningCertificatesPagePage.d.ts.map +0 -1
  129. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +0 -111
  130. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +0 -1
  131. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.d.ts +0 -2
  132. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.d.ts.map +0 -1
  133. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +0 -309
  134. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +0 -1
  135. package/dist/crewfoundry/pages/LearningLearningPagePage.d.ts +0 -2
  136. package/dist/crewfoundry/pages/LearningLearningPagePage.d.ts.map +0 -1
  137. package/dist/crewfoundry/pages/LearningLearningPagePage.js +0 -200
  138. package/dist/crewfoundry/pages/LearningLearningPagePage.js.map +0 -1
  139. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.d.ts +0 -2
  140. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.d.ts.map +0 -1
  141. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +0 -190
  142. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +0 -1
  143. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.d.ts +0 -2
  144. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.d.ts.map +0 -1
  145. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +0 -230
  146. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +0 -1
  147. package/dist/crewfoundry/pages/LearningMyLearningPagePage.d.ts +0 -2
  148. package/dist/crewfoundry/pages/LearningMyLearningPagePage.d.ts.map +0 -1
  149. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +0 -293
  150. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +0 -1
  151. package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.d.ts +0 -2
  152. package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.d.ts.map +0 -1
  153. package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.js +0 -163
  154. package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.js.map +0 -1
  155. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.d.ts +0 -2
  156. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.d.ts.map +0 -1
  157. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js +0 -365
  158. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +0 -1
  159. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.d.ts +0 -2
  160. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.d.ts.map +0 -1
  161. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +0 -224
  162. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +0 -1
  163. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.d.ts +0 -2
  164. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.d.ts.map +0 -1
  165. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +0 -180
  166. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +0 -1
  167. package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.d.ts +0 -2
  168. package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.d.ts.map +0 -1
  169. package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.js +0 -38
  170. package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.js.map +0 -1
  171. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.d.ts +0 -2
  172. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.d.ts.map +0 -1
  173. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +0 -204
  174. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +0 -1
  175. package/dist/crewfoundry/pages/PaymentsEscrowPagePage.d.ts +0 -2
  176. package/dist/crewfoundry/pages/PaymentsEscrowPagePage.d.ts.map +0 -1
  177. package/dist/crewfoundry/pages/PaymentsEscrowPagePage.js +0 -55
  178. package/dist/crewfoundry/pages/PaymentsEscrowPagePage.js.map +0 -1
  179. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.d.ts +0 -2
  180. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.d.ts.map +0 -1
  181. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js +0 -335
  182. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +0 -1
  183. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.d.ts +0 -2
  184. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.d.ts.map +0 -1
  185. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js +0 -160
  186. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +0 -1
  187. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.d.ts +0 -2
  188. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.d.ts.map +0 -1
  189. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js +0 -80
  190. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +0 -1
  191. package/dist/crewfoundry/pages/PaymentsSecurityPagePage.d.ts +0 -2
  192. package/dist/crewfoundry/pages/PaymentsSecurityPagePage.d.ts.map +0 -1
  193. package/dist/crewfoundry/pages/PaymentsSecurityPagePage.js +0 -191
  194. package/dist/crewfoundry/pages/PaymentsSecurityPagePage.js.map +0 -1
  195. package/dist/crewfoundry/utils/courseProgress.d.ts +0 -33
  196. package/dist/crewfoundry/utils/courseProgress.d.ts.map +0 -1
  197. package/dist/crewfoundry/utils/courseProgress.js +0 -46
  198. package/dist/crewfoundry/utils/courseProgress.js.map +0 -1
@@ -1,118 +1,4 @@
1
- import { ACCENT as e } from "./lib/palette.js";
2
- import { cn as t } from "../../utils/cn.js";
3
- import { starPath as n } from "./lib/geometry.js";
4
- import { Download as r } from "lucide-react";
5
- import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
6
- import { Button as s } from "@burdenoff/fe-libs/ui";
7
- //#region src/crewfoundry/components/viz/CertificateCard.tsx
8
- function c() {
9
- return /* @__PURE__ */ o("svg", {
10
- viewBox: "0 0 48 60",
11
- width: 48,
12
- height: 60,
13
- "aria-hidden": "true",
14
- style: {
15
- color: e,
16
- display: "block"
17
- },
18
- children: [
19
- /* @__PURE__ */ a("path", {
20
- d: "M18 38 L18 58 L24 52 L30 58 L30 38 Z",
21
- fill: "currentColor",
22
- opacity: .85
23
- }),
24
- /* @__PURE__ */ a("circle", {
25
- cx: "24",
26
- cy: "22",
27
- r: "19",
28
- fill: "currentColor",
29
- opacity: .1
30
- }),
31
- /* @__PURE__ */ a("circle", {
32
- cx: "24",
33
- cy: "22",
34
- r: "19",
35
- fill: "none",
36
- stroke: "currentColor",
37
- strokeWidth: 2
38
- }),
39
- /* @__PURE__ */ a("circle", {
40
- cx: "24",
41
- cy: "22",
42
- r: "14",
43
- fill: "none",
44
- stroke: "currentColor",
45
- strokeWidth: 1,
46
- opacity: .5
47
- }),
48
- /* @__PURE__ */ a("path", {
49
- d: n(24, 22, 9, 3.8),
50
- fill: "currentColor"
51
- })
52
- ]
53
- });
54
- }
55
- function l({ title: e, recipient: n, date: l, issuer: u, credentialId: d, onDownload: f, downloadLabel: p = "Download", className: m }) {
56
- return /* @__PURE__ */ o("div", {
57
- className: t("relative flex flex-col gap-4 rounded-[var(--radius-card)] border-2 border-border-default bg-bg-surface p-5 shadow-elevation-1", "before:pointer-events-none before:absolute before:inset-1.5 before:rounded-[calc(var(--radius-card)-4px)] before:border before:border-border-subtle before:content-[\"\"]", m),
58
- children: [
59
- /* @__PURE__ */ o("div", {
60
- className: "flex items-start justify-between gap-3",
61
- children: [/* @__PURE__ */ o("div", {
62
- className: "min-w-0",
63
- children: [/* @__PURE__ */ a("p", {
64
- className: "text-xs font-medium uppercase tracking-wide text-text-muted",
65
- children: "Certificate of Completion"
66
- }), /* @__PURE__ */ a("h3", {
67
- className: "mt-1 truncate text-lg font-semibold text-text-primary",
68
- title: e,
69
- children: e
70
- })]
71
- }), /* @__PURE__ */ a(c, {})]
72
- }),
73
- /* @__PURE__ */ o("div", {
74
- className: "min-w-0",
75
- children: [/* @__PURE__ */ a("p", {
76
- className: "text-xs text-text-muted",
77
- children: "Awarded to"
78
- }), /* @__PURE__ */ a("p", {
79
- className: "truncate text-base font-medium text-text-primary",
80
- title: n,
81
- children: n
82
- })]
83
- }),
84
- /* @__PURE__ */ o("div", {
85
- className: "flex flex-wrap items-end justify-between gap-3 border-t border-border-subtle pt-3",
86
- children: [/* @__PURE__ */ o("div", {
87
- className: "flex flex-col gap-0.5 text-xs text-text-muted",
88
- children: [/* @__PURE__ */ o("span", { children: [
89
- "Issued ",
90
- /* @__PURE__ */ a("span", {
91
- className: "text-text-secondary",
92
- children: l
93
- }),
94
- u ? /* @__PURE__ */ o(i, { children: [" ยท ", /* @__PURE__ */ a("span", {
95
- className: "text-text-secondary",
96
- children: u
97
- })] }) : null
98
- ] }), d ? /* @__PURE__ */ o("span", {
99
- className: "tabular-nums",
100
- children: ["ID: ", d]
101
- }) : null]
102
- }), f ? /* @__PURE__ */ o(s, {
103
- variant: "outline",
104
- size: "sm",
105
- onClick: f,
106
- children: [/* @__PURE__ */ a(r, {
107
- "aria-hidden": "true",
108
- className: "h-4 w-4"
109
- }), p]
110
- }) : null]
111
- })
112
- ]
113
- });
114
- }
115
- //#endregion
116
- export { l as CertificateCard };
117
-
118
- //# sourceMappingURL=CertificateCard.js.map
1
+ import "./lib/palette.js";
2
+ import "lucide-react";
3
+ import "react/jsx-runtime";
4
+ import "@burdenoff/fe-libs/ui";
@@ -1,68 +1,3 @@
1
- import { ACCENT as e } from "./lib/palette.js";
2
- import { cn as t } from "../../utils/cn.js";
3
- import { Check as n, Lock as r } from "lucide-react";
4
- import { jsx as i, jsxs as a } from "react/jsx-runtime";
5
- //#region src/crewfoundry/components/viz/PathSteps.tsx
6
- function o({ steps: o, className: s }) {
7
- return o.length === 0 ? null : /* @__PURE__ */ i("ol", {
8
- className: t("flex items-start overflow-x-auto pb-1", s),
9
- "aria-label": "Learning path",
10
- children: o.map((s, c) => {
11
- let l = s.state ?? "todo";
12
- return /* @__PURE__ */ a("li", {
13
- className: "flex min-w-[5.5rem] flex-1 flex-col items-center",
14
- "aria-current": l === "active" ? "step" : void 0,
15
- children: [
16
- /* @__PURE__ */ a("div", {
17
- className: "flex w-full items-center",
18
- children: [
19
- c > 0 ? /* @__PURE__ */ i("span", {
20
- "aria-hidden": "true",
21
- className: "h-0.5 flex-1 rounded-full",
22
- style: { backgroundColor: l === "done" ? e : "var(--color-border-subtle)" }
23
- }) : /* @__PURE__ */ i("span", {
24
- className: "flex-1",
25
- "aria-hidden": "true"
26
- }),
27
- /* @__PURE__ */ i("span", {
28
- className: t("flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors", l === "done" && "border-transparent text-text-inverse", l === "active" && "bg-bg-surface shadow-elevation-1", l === "todo" && "border-border-default bg-bg-surface text-text-secondary", l === "locked" && "border-border-subtle bg-bg-sunken text-text-muted"),
29
- style: l === "done" ? { backgroundColor: e } : l === "active" ? {
30
- borderColor: e,
31
- color: e
32
- } : void 0,
33
- children: l === "done" ? /* @__PURE__ */ i(n, {
34
- "aria-hidden": "true",
35
- className: "h-4 w-4"
36
- }) : l === "locked" ? /* @__PURE__ */ i(r, {
37
- "aria-hidden": "true",
38
- className: "h-3.5 w-3.5"
39
- }) : c + 1
40
- }),
41
- c < o.length - 1 ? /* @__PURE__ */ i("span", {
42
- "aria-hidden": "true",
43
- className: "h-0.5 flex-1 rounded-full",
44
- style: { backgroundColor: l === "done" ? e : "var(--color-border-subtle)" }
45
- }) : /* @__PURE__ */ i("span", {
46
- className: "flex-1",
47
- "aria-hidden": "true"
48
- })
49
- ]
50
- }),
51
- /* @__PURE__ */ i("span", {
52
- className: t("mt-2 max-w-[7rem] truncate px-1 text-center text-xs", l === "locked" ? "text-text-muted" : "font-medium text-text-primary"),
53
- title: s.label,
54
- children: s.label
55
- }),
56
- s.meta ? /* @__PURE__ */ i("span", {
57
- className: "mt-0.5 px-1 text-center text-[11px] text-text-muted",
58
- children: s.meta
59
- }) : null
60
- ]
61
- }, `${s.label}-${c}`);
62
- })
63
- });
64
- }
65
- //#endregion
66
- export { o as PathSteps };
67
-
68
- //# sourceMappingURL=PathSteps.js.map
1
+ import "./lib/palette.js";
2
+ import "lucide-react";
3
+ import "react/jsx-runtime";
@@ -38,32 +38,24 @@ function s(e, t) {
38
38
  let n = o(e), r = e[0], a = e[e.length - 1];
39
39
  return `${n} L${i(a.x)},${i(t)} L${i(r.x)},${i(t)} Z`;
40
40
  }
41
- function c(e, t, n, r, i = 5) {
42
- let a = i * 2, o = [];
43
- for (let i = 0; i < a; i += 1) {
44
- let s = i % 2 == 0 ? n : r;
45
- o.push(l(e, t, s, i * 360 / a));
46
- }
47
- return `${o.map((e, t) => `${t === 0 ? "M" : "L"}${e.x},${e.y}`).join(" ")} Z`;
48
- }
49
- function l(e, t, n, r) {
41
+ function c(e, t, n, r) {
50
42
  let a = (r - 90) * Math.PI / 180;
51
43
  return {
52
44
  x: i(e + n * Math.cos(a)),
53
45
  y: i(t + n * Math.sin(a))
54
46
  };
55
47
  }
56
- function u(e, t, n, r, i, a) {
57
- let o = l(e, t, n, a), s = l(e, t, n, i), c = l(e, t, r, a), u = l(e, t, r, i), d = +(a - i > 180);
48
+ function l(e, t, n, r, i, a) {
49
+ let o = c(e, t, n, a), s = c(e, t, n, i), l = c(e, t, r, a), u = c(e, t, r, i), d = +(a - i > 180);
58
50
  return [
59
51
  `M${o.x},${o.y}`,
60
52
  `A${n},${n} 0 ${d} 0 ${s.x},${s.y}`,
61
53
  `L${u.x},${u.y}`,
62
- `A${r},${r} 0 ${d} 1 ${c.x},${c.y}`,
54
+ `A${r},${r} 0 ${d} 1 ${l.x},${l.y}`,
63
55
  "Z"
64
56
  ].join(" ");
65
57
  }
66
58
  //#endregion
67
- export { s as buildAreaPath, o as buildLinePath, e as clamp, u as donutSegmentPath, t as extent, n as extentWithZero, a as pointsFromValues, l as polarToCartesian, r as scaleLinear, c as starPath };
59
+ export { s as buildAreaPath, o as buildLinePath, e as clamp, l as donutSegmentPath, t as extent, n as extentWithZero, a as pointsFromValues, c as polarToCartesian, r as scaleLinear };
68
60
 
69
61
  //# sourceMappingURL=geometry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"geometry.js","names":[],"sources":["../../../../../src/crewfoundry/components/viz/lib/geometry.ts"],"sourcesContent":["/**\n * Pure geometry / scaling helpers for the CrewFoundry visualization kit.\n * No React, no DOM, no side effects โ€” safe to unit test in isolation.\n */\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** Clamp a value into an inclusive range. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\n/** Sum of a numeric list (0 for empty). */\nexport function sum(values: readonly number[]): number {\n return values.reduce((acc, v) => acc + v, 0);\n}\n\n/** Min/max of a list. Returns [0, 0] for an empty list. */\nexport function extent(values: readonly number[]): [number, number] {\n if (values.length === 0) {\n return [0, 0];\n }\n let min = values[0];\n let max = values[0];\n for (const v of values) {\n if (v < min) {\n min = v;\n }\n if (v > max) {\n max = v;\n }\n }\n return [min, max];\n}\n\n/** Extent that always includes a zero baseline, so bars/areas anchor sensibly. */\nexport function extentWithZero(values: readonly number[]): [number, number] {\n const [min, max] = extent(values);\n return [Math.min(0, min), Math.max(0, max)];\n}\n\n/**\n * Linear map of `value` from a domain onto a range. Degenerate domains\n * (min === max) map to the range midpoint instead of dividing by zero.\n */\nexport function scaleLinear(\n value: number,\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number\n): number {\n if (domainMax === domainMin) {\n return (rangeMin + rangeMax) / 2;\n }\n const t = (value - domainMin) / (domainMax - domainMin);\n return rangeMin + t * (rangeMax - rangeMin);\n}\n\n/** Round to 2 decimals โ€” keeps generated path strings compact. */\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\n/**\n * Evenly space a list of y-values across a box and invert Y (SVG origin is\n * top-left, so larger values render higher).\n */\nexport function pointsFromValues(\n values: readonly number[],\n width: number,\n height: number,\n padding = 0,\n domain?: [number, number]\n): Point[] {\n const n = values.length;\n if (n === 0) {\n return [];\n }\n const [dMin, dMax] = domain ?? extentWithZero(values);\n const innerW = Math.max(0, width - padding * 2);\n const innerH = Math.max(0, height - padding * 2);\n return values.map((v, i) => {\n const x = padding + (n === 1 ? innerW / 2 : (innerW * i) / (n - 1));\n const y = padding + innerH - scaleLinear(v, dMin, dMax, 0, innerH);\n return { x: round(x), y: round(y) };\n });\n}\n\n/** Build an SVG polyline `d` from points. */\nexport function buildLinePath(points: readonly Point[]): string {\n if (points.length === 0) {\n return '';\n }\n return points.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ');\n}\n\n/** Build a closed area `d` (line dropped to a baseline and closed). */\nexport function buildAreaPath(points: readonly Point[], baselineY: number): string {\n if (points.length === 0) {\n return '';\n }\n const line = buildLinePath(points);\n const first = points[0];\n const last = points[points.length - 1];\n return `${line} L${round(last.x)},${round(baselineY)} L${round(first.x)},${round(baselineY)} Z`;\n}\n\n/** SVG path for an n-pointed star centered at (cx, cy). */\nexport function starPath(\n cx: number,\n cy: number,\n outerRadius: number,\n innerRadius: number,\n points = 5\n): string {\n const total = points * 2;\n const coords: Point[] = [];\n for (let i = 0; i < total; i += 1) {\n const radius = i % 2 === 0 ? outerRadius : innerRadius;\n coords.push(polarToCartesian(cx, cy, radius, (i * 360) / total));\n }\n return `${coords.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')} Z`;\n}\n\n/** Convert a polar coordinate (degrees, clockwise from 12 o'clock) to cartesian. */\nexport function polarToCartesian(cx: number, cy: number, radius: number, angleDeg: number): Point {\n const a = ((angleDeg - 90) * Math.PI) / 180;\n return { x: round(cx + radius * Math.cos(a)), y: round(cy + radius * Math.sin(a)) };\n}\n\n/**\n * Annular (donut) segment path between two angles, clockwise from 12 o'clock.\n * For a full 360ยฐ sweep the caller should draw a ring instead (this path\n * degenerates when start and end coincide).\n */\nexport function donutSegmentPath(\n cx: number,\n cy: number,\n rOuter: number,\n rInner: number,\n startAngle: number,\n endAngle: number\n): string {\n const startOuter = polarToCartesian(cx, cy, rOuter, endAngle);\n const endOuter = polarToCartesian(cx, cy, rOuter, startAngle);\n const startInner = polarToCartesian(cx, cy, rInner, endAngle);\n const endInner = polarToCartesian(cx, cy, rInner, startAngle);\n const largeArc = endAngle - startAngle > 180 ? 1 : 0;\n return [\n `M${startOuter.x},${startOuter.y}`,\n `A${rOuter},${rOuter} 0 ${largeArc} 0 ${endOuter.x},${endOuter.y}`,\n `L${endInner.x},${endInner.y}`,\n `A${rInner},${rInner} 0 ${largeArc} 1 ${startInner.x},${startInner.y}`,\n 'Z',\n ].join(' ');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"geometry.js","names":[],"sources":["../../../../../src/crewfoundry/components/viz/lib/geometry.ts"],"sourcesContent":["/**\n * Pure geometry / scaling helpers for the CrewFoundry visualization kit.\n * No React, no DOM, no side effects โ€” safe to unit test in isolation.\n */\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** Clamp a value into an inclusive range. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\n/** Sum of a numeric list (0 for empty). */\nexport function sum(values: readonly number[]): number {\n return values.reduce((acc, v) => acc + v, 0);\n}\n\n/** Min/max of a list. Returns [0, 0] for an empty list. */\nexport function extent(values: readonly number[]): [number, number] {\n if (values.length === 0) {\n return [0, 0];\n }\n let min = values[0];\n let max = values[0];\n for (const v of values) {\n if (v < min) {\n min = v;\n }\n if (v > max) {\n max = v;\n }\n }\n return [min, max];\n}\n\n/** Extent that always includes a zero baseline, so bars/areas anchor sensibly. */\nexport function extentWithZero(values: readonly number[]): [number, number] {\n const [min, max] = extent(values);\n return [Math.min(0, min), Math.max(0, max)];\n}\n\n/**\n * Linear map of `value` from a domain onto a range. Degenerate domains\n * (min === max) map to the range midpoint instead of dividing by zero.\n */\nexport function scaleLinear(\n value: number,\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number\n): number {\n if (domainMax === domainMin) {\n return (rangeMin + rangeMax) / 2;\n }\n const t = (value - domainMin) / (domainMax - domainMin);\n return rangeMin + t * (rangeMax - rangeMin);\n}\n\n/** Round to 2 decimals โ€” keeps generated path strings compact. */\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\n/**\n * Evenly space a list of y-values across a box and invert Y (SVG origin is\n * top-left, so larger values render higher).\n */\nexport function pointsFromValues(\n values: readonly number[],\n width: number,\n height: number,\n padding = 0,\n domain?: [number, number]\n): Point[] {\n const n = values.length;\n if (n === 0) {\n return [];\n }\n const [dMin, dMax] = domain ?? extentWithZero(values);\n const innerW = Math.max(0, width - padding * 2);\n const innerH = Math.max(0, height - padding * 2);\n return values.map((v, i) => {\n const x = padding + (n === 1 ? innerW / 2 : (innerW * i) / (n - 1));\n const y = padding + innerH - scaleLinear(v, dMin, dMax, 0, innerH);\n return { x: round(x), y: round(y) };\n });\n}\n\n/** Build an SVG polyline `d` from points. */\nexport function buildLinePath(points: readonly Point[]): string {\n if (points.length === 0) {\n return '';\n }\n return points.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ');\n}\n\n/** Build a closed area `d` (line dropped to a baseline and closed). */\nexport function buildAreaPath(points: readonly Point[], baselineY: number): string {\n if (points.length === 0) {\n return '';\n }\n const line = buildLinePath(points);\n const first = points[0];\n const last = points[points.length - 1];\n return `${line} L${round(last.x)},${round(baselineY)} L${round(first.x)},${round(baselineY)} Z`;\n}\n\n/** SVG path for an n-pointed star centered at (cx, cy). */\nexport function starPath(\n cx: number,\n cy: number,\n outerRadius: number,\n innerRadius: number,\n points = 5\n): string {\n const total = points * 2;\n const coords: Point[] = [];\n for (let i = 0; i < total; i += 1) {\n const radius = i % 2 === 0 ? outerRadius : innerRadius;\n coords.push(polarToCartesian(cx, cy, radius, (i * 360) / total));\n }\n return `${coords.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')} Z`;\n}\n\n/** Convert a polar coordinate (degrees, clockwise from 12 o'clock) to cartesian. */\nexport function polarToCartesian(cx: number, cy: number, radius: number, angleDeg: number): Point {\n const a = ((angleDeg - 90) * Math.PI) / 180;\n return { x: round(cx + radius * Math.cos(a)), y: round(cy + radius * Math.sin(a)) };\n}\n\n/**\n * Annular (donut) segment path between two angles, clockwise from 12 o'clock.\n * For a full 360ยฐ sweep the caller should draw a ring instead (this path\n * degenerates when start and end coincide).\n */\nexport function donutSegmentPath(\n cx: number,\n cy: number,\n rOuter: number,\n rInner: number,\n startAngle: number,\n endAngle: number\n): string {\n const startOuter = polarToCartesian(cx, cy, rOuter, endAngle);\n const endOuter = polarToCartesian(cx, cy, rOuter, startAngle);\n const startInner = polarToCartesian(cx, cy, rInner, endAngle);\n const endInner = polarToCartesian(cx, cy, rInner, startAngle);\n const largeArc = endAngle - startAngle > 180 ? 1 : 0;\n return [\n `M${startOuter.x},${startOuter.y}`,\n `A${rOuter},${rOuter} 0 ${largeArc} 0 ${endOuter.x},${endOuter.y}`,\n `L${endInner.x},${endInner.y}`,\n `A${rInner},${rInner} 0 ${largeArc} 1 ${startInner.x},${startInner.y}`,\n 'Z',\n ].join(' ');\n}\n"],"mappings":";AAWA,SAAgB,EAAM,GAAe,GAAa,GAAqB;AACrE,QAAO,KAAK,IAAI,GAAK,KAAK,IAAI,GAAK,EAAM,CAAC;;AAS5C,SAAgB,EAAO,GAA6C;AAClE,KAAI,EAAO,WAAW,EACpB,QAAO,CAAC,GAAG,EAAE;CAEf,IAAI,IAAM,EAAO,IACb,IAAM,EAAO;AACjB,MAAK,IAAM,KAAK,EAId,CAHI,IAAI,MACN,IAAM,IAEJ,IAAI,MACN,IAAM;AAGV,QAAO,CAAC,GAAK,EAAI;;AAInB,SAAgB,EAAe,GAA6C;CAC1E,IAAM,CAAC,GAAK,KAAO,EAAO,EAAO;AACjC,QAAO,CAAC,KAAK,IAAI,GAAG,EAAI,EAAE,KAAK,IAAI,GAAG,EAAI,CAAC;;AAO7C,SAAgB,EACd,GACA,GACA,GACA,GACA,GACQ;AAKR,QAJI,MAAc,KACR,IAAW,KAAY,IAG1B,KADI,IAAQ,MAAc,IAAY,MACtB,IAAW;;AAIpC,SAAS,EAAM,GAAmB;AAChC,QAAO,KAAK,MAAM,IAAI,IAAI,GAAG;;AAO/B,SAAgB,EACd,GACA,GACA,GACA,IAAU,GACV,GACS;CACT,IAAM,IAAI,EAAO;AACjB,KAAI,MAAM,EACR,QAAO,EAAE;CAEX,IAAM,CAAC,GAAM,KAAQ,KAAU,EAAe,EAAO,EAC/C,IAAS,KAAK,IAAI,GAAG,IAAQ,IAAU,EAAE,EACzC,IAAS,KAAK,IAAI,GAAG,IAAS,IAAU,EAAE;AAChD,QAAO,EAAO,KAAK,GAAG,MAAM;EAC1B,IAAM,IAAI,KAAW,MAAM,IAAI,IAAS,IAAK,IAAS,KAAM,IAAI,KAC1D,IAAI,IAAU,IAAS,EAAY,GAAG,GAAM,GAAM,GAAG,EAAO;AAClE,SAAO;GAAE,GAAG,EAAM,EAAE;GAAE,GAAG,EAAM,EAAE;GAAE;GACnC;;AAIJ,SAAgB,EAAc,GAAkC;AAI9D,QAHI,EAAO,WAAW,IACb,KAEF,EAAO,KAAK,GAAG,MAAM,GAAG,MAAM,IAAI,MAAM,MAAM,EAAM,EAAE,EAAE,CAAC,GAAG,EAAM,EAAE,EAAE,GAAG,CAAC,KAAK,IAAI;;AAI5F,SAAgB,EAAc,GAA0B,GAA2B;AACjF,KAAI,EAAO,WAAW,EACpB,QAAO;CAET,IAAM,IAAO,EAAc,EAAO,EAC5B,IAAQ,EAAO,IACf,IAAO,EAAO,EAAO,SAAS;AACpC,QAAO,GAAG,EAAK,IAAI,EAAM,EAAK,EAAE,CAAC,GAAG,EAAM,EAAU,CAAC,IAAI,EAAM,EAAM,EAAE,CAAC,GAAG,EAAM,EAAU,CAAC;;AAqB9F,SAAgB,EAAiB,GAAY,GAAY,GAAgB,GAAyB;CAChG,IAAM,KAAM,IAAW,MAAM,KAAK,KAAM;AACxC,QAAO;EAAE,GAAG,EAAM,IAAK,IAAS,KAAK,IAAI,EAAE,CAAC;EAAE,GAAG,EAAM,IAAK,IAAS,KAAK,IAAI,EAAE,CAAC;EAAE;;AAQrF,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACQ;CACR,IAAM,IAAa,EAAiB,GAAI,GAAI,GAAQ,EAAS,EACvD,IAAW,EAAiB,GAAI,GAAI,GAAQ,EAAW,EACvD,IAAa,EAAiB,GAAI,GAAI,GAAQ,EAAS,EACvD,IAAW,EAAiB,GAAI,GAAI,GAAQ,EAAW,EACvD,IAAW,MAAW,IAAa;AACzC,QAAO;EACL,IAAI,EAAW,EAAE,GAAG,EAAW;EAC/B,IAAI,EAAO,GAAG,EAAO,KAAK,EAAS,KAAK,EAAS,EAAE,GAAG,EAAS;EAC/D,IAAI,EAAS,EAAE,GAAG,EAAS;EAC3B,IAAI,EAAO,GAAG,EAAO,KAAK,EAAS,KAAK,EAAW,EAAE,GAAG,EAAW;EACnE;EACD,CAAC,KAAK,IAAI"}
@@ -1,4 +1,4 @@
1
- import { Candidate, Department, Employee, Invoice, JobPosting, PayrollRecord, TimeEntry } from '../store/crewfoundryStore';
1
+ import { Candidate, Department, Employee, JobPosting, PayrollRecord, TimeEntry } from '../store/crewfoundryStore';
2
2
  import { HeatmapRow, LabeledValue, Segment } from '../components/viz';
3
3
 
4
4
  /**
@@ -135,11 +135,6 @@ export interface SkillMatchPanel {
135
135
  * invented, and the panel is empty when the data is absent.
136
136
  */
137
137
  export declare function skillMatch(job: JobPosting, employees: Employee[], candidates: Candidate[], limit?: number): SkillMatchPanel;
138
- /**
139
- * Monthly recognised revenue from PAID invoices, bucketed by `issueDate`.
140
- * Empty when no paid invoice has a parseable issue date.
141
- */
142
- export declare function revenueTrend(invoices: Invoice[]): LabeledValue[];
143
138
  export interface PayrollPeriodTotal {
144
139
  period: string;
145
140
  gross: number;
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/analytics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/analytics.ts"],"names":[],"mappings":"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"}
@@ -276,22 +276,7 @@ function S(e, t, n, r = 6) {
276
276
  source: a
277
277
  };
278
278
  }
279
- function C(e) {
280
- let t = e.filter((e) => e.status === "paid"), n = /* @__PURE__ */ new Map();
281
- for (let e of t) {
282
- let t = r(e.issueDate);
283
- if (!t) continue;
284
- let a = i(t);
285
- n.set(a, (n.get(a) ?? 0) + e.amount);
286
- }
287
- if (n.size === 0) return [];
288
- let o = [...n.keys()].sort((e, t) => e - t), s = Math.floor(o[0] / 12) !== Math.floor(o[o.length - 1] / 12);
289
- return o.map((e) => ({
290
- label: a(e, s),
291
- value: n.get(e) ?? 0
292
- }));
293
- }
294
- function w(e, t = 12) {
279
+ function C(e, t = 12) {
295
280
  let n = e.map((e) => r(e.terminationDate)).filter((e) => e !== null).map(i);
296
281
  if (n.length === 0) return [];
297
282
  let o = Math.max(...n), s = Math.max(Math.min(...n), o - (t - 1)), c = Math.floor(s / 12) !== Math.floor(o / 12), l = [];
@@ -304,7 +289,7 @@ function w(e, t = 12) {
304
289
  }
305
290
  return l;
306
291
  }
307
- function T(e) {
292
+ function w(e) {
308
293
  let t = 0, n = 0;
309
294
  for (let i of e) {
310
295
  let e = r(i.hiredDate), a = r(i.appliedDate);
@@ -318,6 +303,6 @@ function T(e) {
318
303
  };
319
304
  }
320
305
  //#endregion
321
- export { b as allocationByPerson, w as attritionTrend, v as benchAndOverallocated, y as billableSplit, g as deptHeadcount, p as headcountByMonth, m as momDelta, h as performanceDistribution, C as revenueTrend, S as skillMatch, T as timeToHire, _ as utilizationByPersonWeek };
306
+ export { b as allocationByPerson, C as attritionTrend, v as benchAndOverallocated, y as billableSplit, g as deptHeadcount, p as headcountByMonth, m as momDelta, h as performanceDistribution, S as skillMatch, w as timeToHire, _ as utilizationByPersonWeek };
322
307
 
323
308
  //# sourceMappingURL=analytics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","names":[],"sources":["../../../src/crewfoundry/lib/analytics.ts"],"sourcesContent":["/**\n * CrewFoundry real-data analytics helpers.\n *\n * Pure, dependency-free (no React, no store access) transforms that turn REAL\n * store entities into viz-ready data structures for the visualization kit.\n * Every function takes its data as arguments and returns plain data โ€” nothing\n * here reaches for the network, the DOM, or global state, so the results are\n * deterministic and unit-testable.\n *\n * Guiding rule: NEVER fabricate. When a needed field is absent from the backend\n * schema, the corresponding helper returns an empty/blank result (and carries a\n * `// needs backend field: โ€ฆ` note) rather than inventing values.\n */\n\nimport type {\n Candidate,\n Department,\n Employee,\n Invoice,\n JobPosting,\n PayrollRecord,\n TimeEntry,\n} from '../store/crewfoundryStore';\nimport type { HeatmapRow, LabeledValue, Segment } from '../components/viz';\n// Tokenized colors from the (pure, React-free) palette module โ€” importing the\n// viz index here would pull React components into this dependency-free module.\nimport { CHART_GRID, STATUS } from '../components/viz/lib/palette';\n\n// โ”€โ”€ Small pure date helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nconst MONTHS_SHORT = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n] as const;\n\n/** Parse a date-only or ISO string; returns null for empty / \"TBD\" / invalid. */\nfunction parseDate(value?: string | null): Date | null {\n if (!value) {\n return null;\n }\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** Absolute month index (year * 12 + month) for iterating month ranges. */\nfunction monthIndex(date: Date): number {\n return date.getUTCFullYear() * 12 + date.getUTCMonth();\n}\n\nfunction monthLabelFromIndex(index: number, includeYear: boolean): string {\n const year = Math.floor(index / 12);\n const month = index % 12;\n const short = MONTHS_SHORT[month];\n return includeYear ? `${short} ${String(year).slice(2)}` : short;\n}\n\nfunction round(value: number): number {\n return Math.round(value);\n}\n\n// โ”€โ”€ ISO week helpers (used by utilization + allocation) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\ninterface IsoWeek {\n year: number;\n week: number;\n}\n\n/** ISO-8601 week number for a date (weeks start Monday; week 1 owns Jan 4th). */\nfunction isoWeek(date: Date): IsoWeek {\n const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));\n const dayNum = d.getUTCDay() || 7; // Sunday โ†’ 7\n d.setUTCDate(d.getUTCDate() + 4 - dayNum); // shift to the week's Thursday\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);\n return { year: d.getUTCFullYear(), week };\n}\n\nfunction isoWeekKey(date: Date): string {\n const { year, week } = isoWeek(date);\n return `${year}-W${String(week).padStart(2, '0')}`;\n}\n\n/** Monday (UTC) that opens the given ISO week key. */\nfunction isoWeekMonday(key: string): Date | null {\n const match = /^(\\d{4})-W(\\d{2})$/.exec(key);\n if (!match) {\n return null;\n }\n const year = Number(match[1]);\n const week = Number(match[2]);\n const jan4 = new Date(Date.UTC(year, 0, 4));\n const jan4Day = jan4.getUTCDay() || 7;\n const week1Monday = new Date(jan4);\n week1Monday.setUTCDate(jan4.getUTCDate() - (jan4Day - 1));\n const monday = new Date(week1Monday);\n monday.setUTCDate(week1Monday.getUTCDate() + (week - 1) * 7);\n return monday;\n}\n\n/** Compact \"Jul 6โ€“12\" / \"Jun 29 โ€“ Jul 5\" range label for an ISO week key. */\nfunction isoWeekRangeLabel(key: string): string {\n const monday = isoWeekMonday(key);\n if (!monday) {\n return key;\n }\n const sunday = new Date(monday);\n sunday.setUTCDate(monday.getUTCDate() + 6);\n const startMonth = MONTHS_SHORT[monday.getUTCMonth()];\n const endMonth = MONTHS_SHORT[sunday.getUTCMonth()];\n if (monday.getUTCMonth() === sunday.getUTCMonth()) {\n return `${startMonth} ${monday.getUTCDate()}โ€“${sunday.getUTCDate()}`;\n }\n return `${startMonth} ${monday.getUTCDate()} โ€“ ${endMonth} ${sunday.getUTCDate()}`;\n}\n\nconst WEEKLY_CAPACITY_HOURS = 40;\n\nfunction employeeDisplayName(employee: Employee): string {\n return `${employee.firstName} ${employee.lastName}`.trim();\n}\n\n// โ”€โ”€ Headcount โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Cumulative headcount series by month, derived from each employee's `startDate`.\n * Spans the first hire month through the last hire month (inclusive); months\n * with no hires keep the running total flat. Empty when no employee has a\n * parseable start date.\n */\nexport function headcountByMonth(employees: Employee[]): LabeledValue[] {\n const startIndices = employees\n .map((employee) => parseDate(employee.startDate))\n .filter((date): date is Date => date !== null)\n .map(monthIndex);\n\n if (startIndices.length === 0) {\n return [];\n }\n\n const minIndex = Math.min(...startIndices);\n const maxIndex = Math.max(...startIndices);\n const includeYear = Math.floor(minIndex / 12) !== Math.floor(maxIndex / 12);\n\n const series: LabeledValue[] = [];\n for (let index = minIndex; index <= maxIndex; index += 1) {\n const cumulative = startIndices.filter((start) => start <= index).length;\n series.push({ label: monthLabelFromIndex(index, includeYear), value: cumulative });\n }\n return series;\n}\n\nexport interface MomDelta {\n current: number;\n previous: number;\n delta: number;\n deltaPercent: number;\n}\n\n/** Last-vs-prior comparison of a series, for KPI trend indicators. */\nexport function momDelta(series: LabeledValue[]): MomDelta {\n if (series.length === 0) {\n return { current: 0, previous: 0, delta: 0, deltaPercent: 0 };\n }\n const current = series[series.length - 1].value;\n const previous = series.length > 1 ? series[series.length - 2].value : current;\n const delta = current - previous;\n const deltaPercent = previous > 0 ? round((delta / previous) * 100) : 0;\n return { current, previous, delta, deltaPercent };\n}\n\n// โ”€โ”€ Hiring funnel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nconst FUNNEL_STAGES: { key: Candidate['status']; label: string }[] = [\n { key: 'new', label: 'New' },\n { key: 'screening', label: 'Screening' },\n { key: 'interview', label: 'Interview' },\n { key: 'assessment', label: 'Assessment' },\n { key: 'offer', label: 'Offer' },\n { key: 'hired', label: 'Hired' },\n];\n\nexport interface FunnelConversion {\n from: string;\n to: string;\n percent: number;\n}\n\nexport interface HiringFunnel {\n stages: LabeledValue[];\n conversions: FunnelConversion[];\n}\n\n/**\n * Ordered hiring pipeline stages (new โ†’ hired) with the count of candidates\n * currently at each status, plus the stage-to-stage conversion ratio. Counts\n * reflect current status (the backend stores no per-candidate stage history).\n */\nexport function hiringFunnel(candidates: Candidate[]): HiringFunnel {\n const stages: LabeledValue[] = FUNNEL_STAGES.map((stage) => ({\n label: stage.label,\n value: candidates.filter((candidate) => candidate.status === stage.key).length,\n }));\n\n const conversions: FunnelConversion[] = [];\n for (let i = 0; i < stages.length - 1; i += 1) {\n const from = stages[i];\n const to = stages[i + 1];\n conversions.push({\n from: from.label,\n to: to.label,\n percent: from.value > 0 ? round((to.value / from.value) * 100) : 0,\n });\n }\n\n return { stages, conversions };\n}\n\n// โ”€โ”€ Performance distribution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Employee performance-score histogram in four bands derived from the real\n * `performanceScore` (0โ€“5). Employees without a numeric score are excluded.\n */\nexport function performanceDistribution(employees: Employee[]): LabeledValue[] {\n const scores = employees\n .map((employee) => employee.performanceScore)\n .filter((score): score is number => typeof score === 'number' && !Number.isNaN(score));\n\n const buckets: LabeledValue[] = [\n { label: '< 2.5', value: scores.filter((s) => s < 2.5).length },\n { label: '2.5โ€“3.5', value: scores.filter((s) => s >= 2.5 && s < 3.5).length },\n { label: '3.5โ€“4.5', value: scores.filter((s) => s >= 3.5 && s < 4.5).length },\n { label: '4.5+', value: scores.filter((s) => s >= 4.5).length },\n ];\n return buckets;\n}\n\n// โ”€โ”€ Department headcount โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/** Headcount per department (sorted high โ†’ low) from the real `employeeCount`. */\nexport function deptHeadcount(departments: Department[]): LabeledValue[] {\n return departments\n .map((department) => ({ label: department.name, value: department.employeeCount }))\n .sort((a, b) => b.value - a.value);\n}\n\n// โ”€โ”€ Utilization heatmap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nexport interface PersonUtil {\n employeeId: string;\n name: string;\n /** Utilization % per shown week; null where the person logged no time. */\n weekly: (number | null)[];\n /** Mean utilization across the weeks the person actually logged time. */\n avg: number;\n /** Utilization % of the most recent week the person logged time. */\n latest: number;\n}\n\nexport interface UtilizationMatrix {\n /** Heatmap rows (people ร— weeks) ready for the `Heatmap` component. */\n rows: HeatmapRow[];\n /** ISO week keys for the shown columns (ascending). */\n weekKeys: string[];\n /** Short column labels, e.g. \"W1\" โ€ฆ \"W8\". */\n weekLabels: string[];\n /** Human date-range labels aligned to `weekKeys`, e.g. \"Jul 6โ€“12\". */\n weekRanges: string[];\n /** Per-person utilization, aligned to `rows`. */\n personUtil: PersonUtil[];\n hasData: boolean;\n}\n\n/**\n * Per-person, per-ISO-week utilization (ฮฃ totalHours รท 40h capacity) built from\n * real time entries. Columns are only the weeks that actually contain entries\n * (optionally capped to the most recent `weeks`); a person's cell is left blank\n * for any week they logged no time โ€” never fabricated. Rows are sorted by\n * average utilization (busiest first).\n */\nexport function utilizationByPersonWeek(\n timeEntries: TimeEntry[],\n employees: Employee[],\n weeks?: number\n): UtilizationMatrix {\n const employeeById = new Map(employees.map((employee) => [employee.id, employee]));\n\n // employeeId -> weekKey -> summed hours\n const hoursByPersonWeek = new Map<string, Map<string, number>>();\n const weekKeySet = new Set<string>();\n\n for (const entry of timeEntries) {\n const date = parseDate(entry.date);\n if (!date || typeof entry.totalHours !== 'number' || Number.isNaN(entry.totalHours)) {\n continue;\n }\n const key = isoWeekKey(date);\n weekKeySet.add(key);\n let weekMap = hoursByPersonWeek.get(entry.employeeId);\n if (!weekMap) {\n weekMap = new Map<string, number>();\n hoursByPersonWeek.set(entry.employeeId, weekMap);\n }\n weekMap.set(key, (weekMap.get(key) ?? 0) + entry.totalHours);\n }\n\n const allWeekKeys = [...weekKeySet].sort();\n const shownKeys =\n typeof weeks === 'number' && weeks > 0 ? allWeekKeys.slice(-weeks) : allWeekKeys;\n\n if (shownKeys.length === 0 || hoursByPersonWeek.size === 0) {\n return {\n rows: [],\n weekKeys: [],\n weekLabels: [],\n weekRanges: [],\n personUtil: [],\n hasData: false,\n };\n }\n\n const weekLabels = shownKeys.map((_key, index) => `W${index + 1}`);\n const weekRanges = shownKeys.map((key) => isoWeekRangeLabel(key));\n\n const people: PersonUtil[] = [];\n for (const [employeeId, weekMap] of hoursByPersonWeek) {\n const weekly = shownKeys.map((key) => {\n const hours = weekMap.get(key);\n if (hours === undefined) {\n return null;\n }\n return round((hours / WEEKLY_CAPACITY_HOURS) * 100);\n });\n const logged = weekly.filter((value): value is number => value !== null);\n if (logged.length === 0) {\n continue; // person has entries, but none within the shown window\n }\n const avg = round(logged.reduce((sum, value) => sum + value, 0) / logged.length);\n let latest = logged[logged.length - 1];\n for (let i = weekly.length - 1; i >= 0; i -= 1) {\n const value = weekly[i];\n if (value !== null) {\n latest = value;\n break;\n }\n }\n const employee = employeeById.get(employeeId);\n const name = employee\n ? employeeDisplayName(employee)\n : (timeEntries.find((entry) => entry.employeeId === employeeId)?.employeeName ?? 'Unknown');\n people.push({ employeeId, name, weekly, avg, latest });\n }\n\n people.sort((a, b) => b.avg - a.avg);\n\n const rows: HeatmapRow[] = people.map((person) => ({\n label: person.name,\n cells: person.weekly.map((value) => ({ value: value ?? 0 })),\n }));\n\n return {\n rows,\n weekKeys: shownKeys,\n weekLabels,\n weekRanges,\n personUtil: people,\n hasData: rows.length > 0,\n };\n}\n\nexport interface ResourcingKpis {\n /** Mean utilization across allocated people (%). */\n avgUtilization: number;\n /** Active employees with no logged time in the window. */\n bench: number;\n /** Allocated people whose most recent week exceeds 100%. */\n overAllocated: number;\n /** People with any logged time in the window. */\n allocatedCount: number;\n}\n\n/**\n * KPI counts derived from a utilization matrix + the employee roster. `bench`\n * needs the full active roster (people absent from the matrix have no\n * allocation), so the roster is passed alongside the matrix.\n */\nexport function benchAndOverallocated(\n matrix: UtilizationMatrix,\n employees: Employee[]\n): ResourcingKpis {\n const allocatedIds = new Set(matrix.personUtil.map((person) => person.employeeId));\n const activeEmployees = employees.filter((employee) => employee.status === 'active');\n const bench = activeEmployees.filter((employee) => !allocatedIds.has(employee.id)).length;\n const overAllocated = matrix.personUtil.filter((person) => person.latest > 100).length;\n const avgUtilization =\n matrix.personUtil.length > 0\n ? round(\n matrix.personUtil.reduce((sum, person) => sum + person.avg, 0) / matrix.personUtil.length\n )\n : 0;\n return {\n avgUtilization,\n bench,\n overAllocated,\n allocatedCount: matrix.personUtil.length,\n };\n}\n\n// โ”€โ”€ Billable split โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Billable vs non-billable hours donut, summing real `totalHours` split by the\n * real `TimeEntry.billable` flag. Returns null (callers render an empty state)\n * when no logged hours exist โ€” never a fabricated ratio.\n */\nexport function billableSplit(timeEntries: TimeEntry[]): Segment[] | null {\n let billableHours = 0;\n let nonBillableHours = 0;\n for (const entry of timeEntries) {\n const hours =\n typeof entry.totalHours === 'number' && !Number.isNaN(entry.totalHours)\n ? entry.totalHours\n : 0;\n if (entry.billable) {\n billableHours += hours;\n } else {\n nonBillableHours += hours;\n }\n }\n\n if (billableHours + nonBillableHours <= 0) {\n return null;\n }\n\n return [\n { label: 'Billable', value: billableHours, color: STATUS.success },\n { label: 'Non-billable', value: nonBillableHours, color: CHART_GRID },\n ];\n}\n\n// โ”€โ”€ Per-person allocation (latest week) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nexport interface AllocationRow {\n employeeId: string;\n name: string;\n /** One segment per project the person logged time against this week. */\n segments: Segment[];\n /** Utilization % for the week (ฮฃ hours รท 40h). */\n utilization: number;\n /** Human label for the week the allocation covers. */\n weekLabel: string;\n}\n\n/**\n * Per-person project allocation for the most recent ISO week that has real time\n * entries: a stacked bar of hours-by-project plus a utilization %. Empty when\n * no time entries exist. Sorted by utilization (busiest first).\n */\nexport function allocationByPerson(\n timeEntries: TimeEntry[],\n employees: Employee[]\n): AllocationRow[] {\n const dated = timeEntries\n .map((entry) => ({ entry, date: parseDate(entry.date) }))\n .filter(\n (item): item is { entry: TimeEntry; date: Date } =>\n item.date !== null &&\n typeof item.entry.totalHours === 'number' &&\n !Number.isNaN(item.entry.totalHours)\n );\n\n if (dated.length === 0) {\n return [];\n }\n\n const latestKey = dated\n .map((item) => isoWeekKey(item.date))\n .sort()\n .slice(-1)[0];\n const weekLabel = isoWeekRangeLabel(latestKey);\n const employeeById = new Map(employees.map((employee) => [employee.id, employee]));\n\n // employeeId -> project -> hours\n const byPerson = new Map<string, Map<string, number>>();\n const nameByPerson = new Map<string, string>();\n\n for (const { entry, date } of dated) {\n if (isoWeekKey(date) !== latestKey) {\n continue;\n }\n let projectMap = byPerson.get(entry.employeeId);\n if (!projectMap) {\n projectMap = new Map<string, number>();\n byPerson.set(entry.employeeId, projectMap);\n }\n const project = entry.project || 'Unassigned';\n projectMap.set(project, (projectMap.get(project) ?? 0) + entry.totalHours);\n const employee = employeeById.get(entry.employeeId);\n nameByPerson.set(\n entry.employeeId,\n employee ? employeeDisplayName(employee) : entry.employeeName || 'Unknown'\n );\n }\n\n const rows: AllocationRow[] = [];\n for (const [employeeId, projectMap] of byPerson) {\n const segments: Segment[] = [...projectMap.entries()].map(([label, value]) => ({\n label,\n value,\n }));\n const totalHours = segments.reduce((sum, segment) => sum + segment.value, 0);\n rows.push({\n employeeId,\n name: nameByPerson.get(employeeId) ?? 'Unknown',\n segments,\n utilization: round((totalHours / WEEKLY_CAPACITY_HOURS) * 100),\n weekLabel,\n });\n }\n\n rows.sort((a, b) => b.utilization - a.utilization);\n return rows;\n}\n\n// โ”€โ”€ Skills match โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/** Which real pool the skill matches were drawn from. */\nexport type SkillMatchSource = 'employee' | 'candidate';\n\nexport interface SkillMatchResult {\n id: string;\n name: string;\n matchPercent: number;\n matchedSkills: string[];\n missingSkills: string[];\n /** Short descriptor: role ยท department (employees) or experience ยท status (candidates). */\n detail: string;\n}\n\nexport interface SkillMatchPanel {\n roleTitle: string;\n /** Real skill tags the role calls for, derived from the matched pool's skill universe. */\n requiredSkills: string[];\n matches: SkillMatchResult[];\n /** Whether matches came from internal employees or the candidate pool. */\n source: SkillMatchSource;\n}\n\n/** A person (employee or candidate) reduced to what skill matching needs. */\ninterface MatchablePerson {\n id: string;\n name: string;\n skills: string[];\n detail: string;\n}\n\n/** \"3 yrs experience ยท Interview\" descriptor for a candidate (no UI helpers in this pure module). */\nfunction candidateDetail(candidate: Candidate): string {\n const years = candidate.experience;\n const experience = `${years} yr${years === 1 ? '' : 's'} experience`;\n const status = candidate.status.replace(/_/g, ' ');\n return `${experience} ยท ${status}`;\n}\n\n/**\n * Rank people by how well their real skill tags overlap a role's required\n * skills. Prefers internal employees (`Employee.skills`); falls back to the\n * candidate pool when no employee has skills recorded. The role's required\n * skills are themselves derived from real data โ€” the matched pool's skill\n * universe intersected with the job's title + requirement text โ€” so nothing is\n * invented, and the panel is empty when the data is absent.\n */\nexport function skillMatch(\n job: JobPosting,\n employees: Employee[],\n candidates: Candidate[],\n limit = 6\n): SkillMatchPanel {\n const employeePeople: MatchablePerson[] = employees\n .filter((employee) => employee.skills.length > 0)\n .map((employee) => ({\n id: employee.id,\n name: `${employee.firstName} ${employee.lastName}`.trim(),\n skills: employee.skills,\n detail: [employee.position, employee.department].filter(Boolean).join(' ยท '),\n }));\n\n const source: SkillMatchSource = employeePeople.length > 0 ? 'employee' : 'candidate';\n const people: MatchablePerson[] =\n source === 'employee'\n ? employeePeople\n : candidates.map((candidate) => ({\n id: candidate.id,\n name: `${candidate.firstName} ${candidate.lastName}`.trim(),\n skills: candidate.skills,\n detail: candidateDetail(candidate),\n }));\n\n // Distinct skill tags across the chosen pool, preserving first-seen casing.\n const universe = new Map<string, string>();\n for (const person of people) {\n for (const skill of person.skills) {\n const normalized = skill.trim();\n if (normalized.length >= 2 && !universe.has(normalized.toLowerCase())) {\n universe.set(normalized.toLowerCase(), normalized);\n }\n }\n }\n\n const haystack = `${job.title} ${job.requirements.join(' ')}`.toLowerCase();\n const requiredSkills = [...universe.entries()]\n .filter(([lower]) => haystack.includes(lower))\n .map(([, original]) => original);\n\n if (requiredSkills.length === 0) {\n return { roleTitle: job.title, requiredSkills: [], matches: [], source };\n }\n\n const requiredLower = requiredSkills.map((skill) => skill.toLowerCase());\n\n const matches: SkillMatchResult[] = people\n .map((person) => {\n const personLower = new Set(person.skills.map((skill) => skill.toLowerCase()));\n const matchedSkills = requiredSkills.filter((_skill, index) =>\n personLower.has(requiredLower[index])\n );\n const missingSkills = requiredSkills.filter(\n (_skill, index) => !personLower.has(requiredLower[index])\n );\n return {\n id: person.id,\n name: person.name,\n matchPercent: round((matchedSkills.length / requiredSkills.length) * 100),\n matchedSkills,\n missingSkills,\n detail: person.detail,\n };\n })\n .filter((result) => result.matchedSkills.length > 0)\n .sort(\n (a, b) => b.matchPercent - a.matchPercent || b.matchedSkills.length - a.matchedSkills.length\n )\n .slice(0, limit);\n\n return { roleTitle: job.title, requiredSkills, matches, source };\n}\n\n// โ”€โ”€ Finance / payroll trends (best-effort) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Monthly recognised revenue from PAID invoices, bucketed by `issueDate`.\n * Empty when no paid invoice has a parseable issue date.\n */\nexport function revenueTrend(invoices: Invoice[]): LabeledValue[] {\n const paid = invoices.filter((invoice) => invoice.status === 'paid');\n const totals = new Map<number, number>();\n for (const invoice of paid) {\n const date = parseDate(invoice.issueDate);\n if (!date) {\n continue;\n }\n const index = monthIndex(date);\n totals.set(index, (totals.get(index) ?? 0) + invoice.amount);\n }\n if (totals.size === 0) {\n return [];\n }\n const indices = [...totals.keys()].sort((a, b) => a - b);\n const includeYear = Math.floor(indices[0] / 12) !== Math.floor(indices[indices.length - 1] / 12);\n return indices.map((index) => ({\n label: monthLabelFromIndex(index, includeYear),\n value: totals.get(index) ?? 0,\n }));\n}\n\nexport interface PayrollPeriodTotal {\n period: string;\n gross: number;\n net: number;\n deductions: number;\n}\n\n/**\n * Gross / net / deduction totals per payroll period, in first-seen period order\n * (period is a free-text label so it is not re-sorted). Empty when there are no\n * records.\n */\nexport function payrollTotals(payrollRecords: PayrollRecord[]): PayrollPeriodTotal[] {\n const order: string[] = [];\n const totals = new Map<string, PayrollPeriodTotal>();\n for (const record of payrollRecords) {\n let bucket = totals.get(record.period);\n if (!bucket) {\n bucket = { period: record.period, gross: 0, net: 0, deductions: 0 };\n totals.set(record.period, bucket);\n order.push(record.period);\n }\n bucket.gross += record.grossPay;\n bucket.net += record.netPay;\n bucket.deductions += record.deductions;\n }\n return order.map((period) => totals.get(period) as PayrollPeriodTotal);\n}\n\n/**\n * Monthly attrition: the count of employee exits per month, derived from the\n * real `Employee.terminationDate`, over the most recent `monthsWindow` months\n * that contain an exit (through the latest exit month, inclusive). Months with\n * no exits in that span read zero. Empty when no employee has a termination\n * date โ€” never a fabricated exit month.\n */\nexport function attritionTrend(employees: Employee[], monthsWindow = 12): LabeledValue[] {\n const exitIndices = employees\n .map((employee) => parseDate(employee.terminationDate))\n .filter((date): date is Date => date !== null)\n .map(monthIndex);\n\n if (exitIndices.length === 0) {\n return [];\n }\n\n const maxIndex = Math.max(...exitIndices);\n const minIndex = Math.max(Math.min(...exitIndices), maxIndex - (monthsWindow - 1));\n const includeYear = Math.floor(minIndex / 12) !== Math.floor(maxIndex / 12);\n\n const series: LabeledValue[] = [];\n for (let index = minIndex; index <= maxIndex; index += 1) {\n const count = exitIndices.filter((exit) => exit === index).length;\n series.push({ label: monthLabelFromIndex(index, includeYear), value: count });\n }\n return series;\n}\n\nexport interface TimeToHire {\n averageDays: number;\n sampleSize: number;\n}\n\n/**\n * Average time-to-hire: mean elapsed days from `appliedDate` to the real\n * `Candidate.hiredDate`, across candidates that carry both dates. Non-negative\n * spans only. Returns null (callers render an empty state) when no candidate\n * has a hired date โ€” the elapsed time is never estimated.\n */\nexport function timeToHire(candidates: Candidate[]): TimeToHire | null {\n let totalDays = 0;\n let sampleSize = 0;\n for (const candidate of candidates) {\n const hired = parseDate(candidate.hiredDate);\n const applied = parseDate(candidate.appliedDate);\n if (!hired || !applied) {\n continue;\n }\n const days = Math.round((hired.getTime() - applied.getTime()) / 86_400_000);\n if (days < 0) {\n continue;\n }\n totalDays += days;\n sampleSize += 1;\n }\n\n if (sampleSize === 0) {\n return null;\n }\n\n return { averageDays: Math.round(totalDays / sampleSize), sampleSize };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"analytics.js","names":[],"sources":["../../../src/crewfoundry/lib/analytics.ts"],"sourcesContent":["/**\n * CrewFoundry real-data analytics helpers.\n *\n * Pure, dependency-free (no React, no store access) transforms that turn REAL\n * store entities into viz-ready data structures for the visualization kit.\n * Every function takes its data as arguments and returns plain data โ€” nothing\n * here reaches for the network, the DOM, or global state, so the results are\n * deterministic and unit-testable.\n *\n * Guiding rule: NEVER fabricate. When a needed field is absent from the backend\n * schema, the corresponding helper returns an empty/blank result (and carries a\n * `// needs backend field: โ€ฆ` note) rather than inventing values.\n */\n\nimport type {\n Candidate,\n Department,\n Employee,\n JobPosting,\n PayrollRecord,\n TimeEntry,\n} from '../store/crewfoundryStore';\nimport type { HeatmapRow, LabeledValue, Segment } from '../components/viz';\n// Tokenized colors from the (pure, React-free) palette module โ€” importing the\n// viz index here would pull React components into this dependency-free module.\nimport { CHART_GRID, STATUS } from '../components/viz/lib/palette';\n\n// โ”€โ”€ Small pure date helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nconst MONTHS_SHORT = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n] as const;\n\n/** Parse a date-only or ISO string; returns null for empty / \"TBD\" / invalid. */\nfunction parseDate(value?: string | null): Date | null {\n if (!value) {\n return null;\n }\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** Absolute month index (year * 12 + month) for iterating month ranges. */\nfunction monthIndex(date: Date): number {\n return date.getUTCFullYear() * 12 + date.getUTCMonth();\n}\n\nfunction monthLabelFromIndex(index: number, includeYear: boolean): string {\n const year = Math.floor(index / 12);\n const month = index % 12;\n const short = MONTHS_SHORT[month];\n return includeYear ? `${short} ${String(year).slice(2)}` : short;\n}\n\nfunction round(value: number): number {\n return Math.round(value);\n}\n\n// โ”€โ”€ ISO week helpers (used by utilization + allocation) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\ninterface IsoWeek {\n year: number;\n week: number;\n}\n\n/** ISO-8601 week number for a date (weeks start Monday; week 1 owns Jan 4th). */\nfunction isoWeek(date: Date): IsoWeek {\n const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));\n const dayNum = d.getUTCDay() || 7; // Sunday โ†’ 7\n d.setUTCDate(d.getUTCDate() + 4 - dayNum); // shift to the week's Thursday\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);\n return { year: d.getUTCFullYear(), week };\n}\n\nfunction isoWeekKey(date: Date): string {\n const { year, week } = isoWeek(date);\n return `${year}-W${String(week).padStart(2, '0')}`;\n}\n\n/** Monday (UTC) that opens the given ISO week key. */\nfunction isoWeekMonday(key: string): Date | null {\n const match = /^(\\d{4})-W(\\d{2})$/.exec(key);\n if (!match) {\n return null;\n }\n const year = Number(match[1]);\n const week = Number(match[2]);\n const jan4 = new Date(Date.UTC(year, 0, 4));\n const jan4Day = jan4.getUTCDay() || 7;\n const week1Monday = new Date(jan4);\n week1Monday.setUTCDate(jan4.getUTCDate() - (jan4Day - 1));\n const monday = new Date(week1Monday);\n monday.setUTCDate(week1Monday.getUTCDate() + (week - 1) * 7);\n return monday;\n}\n\n/** Compact \"Jul 6โ€“12\" / \"Jun 29 โ€“ Jul 5\" range label for an ISO week key. */\nfunction isoWeekRangeLabel(key: string): string {\n const monday = isoWeekMonday(key);\n if (!monday) {\n return key;\n }\n const sunday = new Date(monday);\n sunday.setUTCDate(monday.getUTCDate() + 6);\n const startMonth = MONTHS_SHORT[monday.getUTCMonth()];\n const endMonth = MONTHS_SHORT[sunday.getUTCMonth()];\n if (monday.getUTCMonth() === sunday.getUTCMonth()) {\n return `${startMonth} ${monday.getUTCDate()}โ€“${sunday.getUTCDate()}`;\n }\n return `${startMonth} ${monday.getUTCDate()} โ€“ ${endMonth} ${sunday.getUTCDate()}`;\n}\n\nconst WEEKLY_CAPACITY_HOURS = 40;\n\nfunction employeeDisplayName(employee: Employee): string {\n return `${employee.firstName} ${employee.lastName}`.trim();\n}\n\n// โ”€โ”€ Headcount โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Cumulative headcount series by month, derived from each employee's `startDate`.\n * Spans the first hire month through the last hire month (inclusive); months\n * with no hires keep the running total flat. Empty when no employee has a\n * parseable start date.\n */\nexport function headcountByMonth(employees: Employee[]): LabeledValue[] {\n const startIndices = employees\n .map((employee) => parseDate(employee.startDate))\n .filter((date): date is Date => date !== null)\n .map(monthIndex);\n\n if (startIndices.length === 0) {\n return [];\n }\n\n const minIndex = Math.min(...startIndices);\n const maxIndex = Math.max(...startIndices);\n const includeYear = Math.floor(minIndex / 12) !== Math.floor(maxIndex / 12);\n\n const series: LabeledValue[] = [];\n for (let index = minIndex; index <= maxIndex; index += 1) {\n const cumulative = startIndices.filter((start) => start <= index).length;\n series.push({ label: monthLabelFromIndex(index, includeYear), value: cumulative });\n }\n return series;\n}\n\nexport interface MomDelta {\n current: number;\n previous: number;\n delta: number;\n deltaPercent: number;\n}\n\n/** Last-vs-prior comparison of a series, for KPI trend indicators. */\nexport function momDelta(series: LabeledValue[]): MomDelta {\n if (series.length === 0) {\n return { current: 0, previous: 0, delta: 0, deltaPercent: 0 };\n }\n const current = series[series.length - 1].value;\n const previous = series.length > 1 ? series[series.length - 2].value : current;\n const delta = current - previous;\n const deltaPercent = previous > 0 ? round((delta / previous) * 100) : 0;\n return { current, previous, delta, deltaPercent };\n}\n\n// โ”€โ”€ Hiring funnel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nconst FUNNEL_STAGES: { key: Candidate['status']; label: string }[] = [\n { key: 'new', label: 'New' },\n { key: 'screening', label: 'Screening' },\n { key: 'interview', label: 'Interview' },\n { key: 'assessment', label: 'Assessment' },\n { key: 'offer', label: 'Offer' },\n { key: 'hired', label: 'Hired' },\n];\n\nexport interface FunnelConversion {\n from: string;\n to: string;\n percent: number;\n}\n\nexport interface HiringFunnel {\n stages: LabeledValue[];\n conversions: FunnelConversion[];\n}\n\n/**\n * Ordered hiring pipeline stages (new โ†’ hired) with the count of candidates\n * currently at each status, plus the stage-to-stage conversion ratio. Counts\n * reflect current status (the backend stores no per-candidate stage history).\n */\nexport function hiringFunnel(candidates: Candidate[]): HiringFunnel {\n const stages: LabeledValue[] = FUNNEL_STAGES.map((stage) => ({\n label: stage.label,\n value: candidates.filter((candidate) => candidate.status === stage.key).length,\n }));\n\n const conversions: FunnelConversion[] = [];\n for (let i = 0; i < stages.length - 1; i += 1) {\n const from = stages[i];\n const to = stages[i + 1];\n conversions.push({\n from: from.label,\n to: to.label,\n percent: from.value > 0 ? round((to.value / from.value) * 100) : 0,\n });\n }\n\n return { stages, conversions };\n}\n\n// โ”€โ”€ Performance distribution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Employee performance-score histogram in four bands derived from the real\n * `performanceScore` (0โ€“5). Employees without a numeric score are excluded.\n */\nexport function performanceDistribution(employees: Employee[]): LabeledValue[] {\n const scores = employees\n .map((employee) => employee.performanceScore)\n .filter((score): score is number => typeof score === 'number' && !Number.isNaN(score));\n\n const buckets: LabeledValue[] = [\n { label: '< 2.5', value: scores.filter((s) => s < 2.5).length },\n { label: '2.5โ€“3.5', value: scores.filter((s) => s >= 2.5 && s < 3.5).length },\n { label: '3.5โ€“4.5', value: scores.filter((s) => s >= 3.5 && s < 4.5).length },\n { label: '4.5+', value: scores.filter((s) => s >= 4.5).length },\n ];\n return buckets;\n}\n\n// โ”€โ”€ Department headcount โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/** Headcount per department (sorted high โ†’ low) from the real `employeeCount`. */\nexport function deptHeadcount(departments: Department[]): LabeledValue[] {\n return departments\n .map((department) => ({ label: department.name, value: department.employeeCount }))\n .sort((a, b) => b.value - a.value);\n}\n\n// โ”€โ”€ Utilization heatmap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nexport interface PersonUtil {\n employeeId: string;\n name: string;\n /** Utilization % per shown week; null where the person logged no time. */\n weekly: (number | null)[];\n /** Mean utilization across the weeks the person actually logged time. */\n avg: number;\n /** Utilization % of the most recent week the person logged time. */\n latest: number;\n}\n\nexport interface UtilizationMatrix {\n /** Heatmap rows (people ร— weeks) ready for the `Heatmap` component. */\n rows: HeatmapRow[];\n /** ISO week keys for the shown columns (ascending). */\n weekKeys: string[];\n /** Short column labels, e.g. \"W1\" โ€ฆ \"W8\". */\n weekLabels: string[];\n /** Human date-range labels aligned to `weekKeys`, e.g. \"Jul 6โ€“12\". */\n weekRanges: string[];\n /** Per-person utilization, aligned to `rows`. */\n personUtil: PersonUtil[];\n hasData: boolean;\n}\n\n/**\n * Per-person, per-ISO-week utilization (ฮฃ totalHours รท 40h capacity) built from\n * real time entries. Columns are only the weeks that actually contain entries\n * (optionally capped to the most recent `weeks`); a person's cell is left blank\n * for any week they logged no time โ€” never fabricated. Rows are sorted by\n * average utilization (busiest first).\n */\nexport function utilizationByPersonWeek(\n timeEntries: TimeEntry[],\n employees: Employee[],\n weeks?: number\n): UtilizationMatrix {\n const employeeById = new Map(employees.map((employee) => [employee.id, employee]));\n\n // employeeId -> weekKey -> summed hours\n const hoursByPersonWeek = new Map<string, Map<string, number>>();\n const weekKeySet = new Set<string>();\n\n for (const entry of timeEntries) {\n const date = parseDate(entry.date);\n if (!date || typeof entry.totalHours !== 'number' || Number.isNaN(entry.totalHours)) {\n continue;\n }\n const key = isoWeekKey(date);\n weekKeySet.add(key);\n let weekMap = hoursByPersonWeek.get(entry.employeeId);\n if (!weekMap) {\n weekMap = new Map<string, number>();\n hoursByPersonWeek.set(entry.employeeId, weekMap);\n }\n weekMap.set(key, (weekMap.get(key) ?? 0) + entry.totalHours);\n }\n\n const allWeekKeys = [...weekKeySet].sort();\n const shownKeys =\n typeof weeks === 'number' && weeks > 0 ? allWeekKeys.slice(-weeks) : allWeekKeys;\n\n if (shownKeys.length === 0 || hoursByPersonWeek.size === 0) {\n return {\n rows: [],\n weekKeys: [],\n weekLabels: [],\n weekRanges: [],\n personUtil: [],\n hasData: false,\n };\n }\n\n const weekLabels = shownKeys.map((_key, index) => `W${index + 1}`);\n const weekRanges = shownKeys.map((key) => isoWeekRangeLabel(key));\n\n const people: PersonUtil[] = [];\n for (const [employeeId, weekMap] of hoursByPersonWeek) {\n const weekly = shownKeys.map((key) => {\n const hours = weekMap.get(key);\n if (hours === undefined) {\n return null;\n }\n return round((hours / WEEKLY_CAPACITY_HOURS) * 100);\n });\n const logged = weekly.filter((value): value is number => value !== null);\n if (logged.length === 0) {\n continue; // person has entries, but none within the shown window\n }\n const avg = round(logged.reduce((sum, value) => sum + value, 0) / logged.length);\n let latest = logged[logged.length - 1];\n for (let i = weekly.length - 1; i >= 0; i -= 1) {\n const value = weekly[i];\n if (value !== null) {\n latest = value;\n break;\n }\n }\n const employee = employeeById.get(employeeId);\n const name = employee\n ? employeeDisplayName(employee)\n : (timeEntries.find((entry) => entry.employeeId === employeeId)?.employeeName ?? 'Unknown');\n people.push({ employeeId, name, weekly, avg, latest });\n }\n\n people.sort((a, b) => b.avg - a.avg);\n\n const rows: HeatmapRow[] = people.map((person) => ({\n label: person.name,\n cells: person.weekly.map((value) => ({ value: value ?? 0 })),\n }));\n\n return {\n rows,\n weekKeys: shownKeys,\n weekLabels,\n weekRanges,\n personUtil: people,\n hasData: rows.length > 0,\n };\n}\n\nexport interface ResourcingKpis {\n /** Mean utilization across allocated people (%). */\n avgUtilization: number;\n /** Active employees with no logged time in the window. */\n bench: number;\n /** Allocated people whose most recent week exceeds 100%. */\n overAllocated: number;\n /** People with any logged time in the window. */\n allocatedCount: number;\n}\n\n/**\n * KPI counts derived from a utilization matrix + the employee roster. `bench`\n * needs the full active roster (people absent from the matrix have no\n * allocation), so the roster is passed alongside the matrix.\n */\nexport function benchAndOverallocated(\n matrix: UtilizationMatrix,\n employees: Employee[]\n): ResourcingKpis {\n const allocatedIds = new Set(matrix.personUtil.map((person) => person.employeeId));\n const activeEmployees = employees.filter((employee) => employee.status === 'active');\n const bench = activeEmployees.filter((employee) => !allocatedIds.has(employee.id)).length;\n const overAllocated = matrix.personUtil.filter((person) => person.latest > 100).length;\n const avgUtilization =\n matrix.personUtil.length > 0\n ? round(\n matrix.personUtil.reduce((sum, person) => sum + person.avg, 0) / matrix.personUtil.length\n )\n : 0;\n return {\n avgUtilization,\n bench,\n overAllocated,\n allocatedCount: matrix.personUtil.length,\n };\n}\n\n// โ”€โ”€ Billable split โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/**\n * Billable vs non-billable hours donut, summing real `totalHours` split by the\n * real `TimeEntry.billable` flag. Returns null (callers render an empty state)\n * when no logged hours exist โ€” never a fabricated ratio.\n */\nexport function billableSplit(timeEntries: TimeEntry[]): Segment[] | null {\n let billableHours = 0;\n let nonBillableHours = 0;\n for (const entry of timeEntries) {\n const hours =\n typeof entry.totalHours === 'number' && !Number.isNaN(entry.totalHours)\n ? entry.totalHours\n : 0;\n if (entry.billable) {\n billableHours += hours;\n } else {\n nonBillableHours += hours;\n }\n }\n\n if (billableHours + nonBillableHours <= 0) {\n return null;\n }\n\n return [\n { label: 'Billable', value: billableHours, color: STATUS.success },\n { label: 'Non-billable', value: nonBillableHours, color: CHART_GRID },\n ];\n}\n\n// โ”€โ”€ Per-person allocation (latest week) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nexport interface AllocationRow {\n employeeId: string;\n name: string;\n /** One segment per project the person logged time against this week. */\n segments: Segment[];\n /** Utilization % for the week (ฮฃ hours รท 40h). */\n utilization: number;\n /** Human label for the week the allocation covers. */\n weekLabel: string;\n}\n\n/**\n * Per-person project allocation for the most recent ISO week that has real time\n * entries: a stacked bar of hours-by-project plus a utilization %. Empty when\n * no time entries exist. Sorted by utilization (busiest first).\n */\nexport function allocationByPerson(\n timeEntries: TimeEntry[],\n employees: Employee[]\n): AllocationRow[] {\n const dated = timeEntries\n .map((entry) => ({ entry, date: parseDate(entry.date) }))\n .filter(\n (item): item is { entry: TimeEntry; date: Date } =>\n item.date !== null &&\n typeof item.entry.totalHours === 'number' &&\n !Number.isNaN(item.entry.totalHours)\n );\n\n if (dated.length === 0) {\n return [];\n }\n\n const latestKey = dated\n .map((item) => isoWeekKey(item.date))\n .sort()\n .slice(-1)[0];\n const weekLabel = isoWeekRangeLabel(latestKey);\n const employeeById = new Map(employees.map((employee) => [employee.id, employee]));\n\n // employeeId -> project -> hours\n const byPerson = new Map<string, Map<string, number>>();\n const nameByPerson = new Map<string, string>();\n\n for (const { entry, date } of dated) {\n if (isoWeekKey(date) !== latestKey) {\n continue;\n }\n let projectMap = byPerson.get(entry.employeeId);\n if (!projectMap) {\n projectMap = new Map<string, number>();\n byPerson.set(entry.employeeId, projectMap);\n }\n const project = entry.project || 'Unassigned';\n projectMap.set(project, (projectMap.get(project) ?? 0) + entry.totalHours);\n const employee = employeeById.get(entry.employeeId);\n nameByPerson.set(\n entry.employeeId,\n employee ? employeeDisplayName(employee) : entry.employeeName || 'Unknown'\n );\n }\n\n const rows: AllocationRow[] = [];\n for (const [employeeId, projectMap] of byPerson) {\n const segments: Segment[] = [...projectMap.entries()].map(([label, value]) => ({\n label,\n value,\n }));\n const totalHours = segments.reduce((sum, segment) => sum + segment.value, 0);\n rows.push({\n employeeId,\n name: nameByPerson.get(employeeId) ?? 'Unknown',\n segments,\n utilization: round((totalHours / WEEKLY_CAPACITY_HOURS) * 100),\n weekLabel,\n });\n }\n\n rows.sort((a, b) => b.utilization - a.utilization);\n return rows;\n}\n\n// โ”€โ”€ Skills match โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\n/** Which real pool the skill matches were drawn from. */\nexport type SkillMatchSource = 'employee' | 'candidate';\n\nexport interface SkillMatchResult {\n id: string;\n name: string;\n matchPercent: number;\n matchedSkills: string[];\n missingSkills: string[];\n /** Short descriptor: role ยท department (employees) or experience ยท status (candidates). */\n detail: string;\n}\n\nexport interface SkillMatchPanel {\n roleTitle: string;\n /** Real skill tags the role calls for, derived from the matched pool's skill universe. */\n requiredSkills: string[];\n matches: SkillMatchResult[];\n /** Whether matches came from internal employees or the candidate pool. */\n source: SkillMatchSource;\n}\n\n/** A person (employee or candidate) reduced to what skill matching needs. */\ninterface MatchablePerson {\n id: string;\n name: string;\n skills: string[];\n detail: string;\n}\n\n/** \"3 yrs experience ยท Interview\" descriptor for a candidate (no UI helpers in this pure module). */\nfunction candidateDetail(candidate: Candidate): string {\n const years = candidate.experience;\n const experience = `${years} yr${years === 1 ? '' : 's'} experience`;\n const status = candidate.status.replace(/_/g, ' ');\n return `${experience} ยท ${status}`;\n}\n\n/**\n * Rank people by how well their real skill tags overlap a role's required\n * skills. Prefers internal employees (`Employee.skills`); falls back to the\n * candidate pool when no employee has skills recorded. The role's required\n * skills are themselves derived from real data โ€” the matched pool's skill\n * universe intersected with the job's title + requirement text โ€” so nothing is\n * invented, and the panel is empty when the data is absent.\n */\nexport function skillMatch(\n job: JobPosting,\n employees: Employee[],\n candidates: Candidate[],\n limit = 6\n): SkillMatchPanel {\n const employeePeople: MatchablePerson[] = employees\n .filter((employee) => employee.skills.length > 0)\n .map((employee) => ({\n id: employee.id,\n name: `${employee.firstName} ${employee.lastName}`.trim(),\n skills: employee.skills,\n detail: [employee.position, employee.department].filter(Boolean).join(' ยท '),\n }));\n\n const source: SkillMatchSource = employeePeople.length > 0 ? 'employee' : 'candidate';\n const people: MatchablePerson[] =\n source === 'employee'\n ? employeePeople\n : candidates.map((candidate) => ({\n id: candidate.id,\n name: `${candidate.firstName} ${candidate.lastName}`.trim(),\n skills: candidate.skills,\n detail: candidateDetail(candidate),\n }));\n\n // Distinct skill tags across the chosen pool, preserving first-seen casing.\n const universe = new Map<string, string>();\n for (const person of people) {\n for (const skill of person.skills) {\n const normalized = skill.trim();\n if (normalized.length >= 2 && !universe.has(normalized.toLowerCase())) {\n universe.set(normalized.toLowerCase(), normalized);\n }\n }\n }\n\n const haystack = `${job.title} ${job.requirements.join(' ')}`.toLowerCase();\n const requiredSkills = [...universe.entries()]\n .filter(([lower]) => haystack.includes(lower))\n .map(([, original]) => original);\n\n if (requiredSkills.length === 0) {\n return { roleTitle: job.title, requiredSkills: [], matches: [], source };\n }\n\n const requiredLower = requiredSkills.map((skill) => skill.toLowerCase());\n\n const matches: SkillMatchResult[] = people\n .map((person) => {\n const personLower = new Set(person.skills.map((skill) => skill.toLowerCase()));\n const matchedSkills = requiredSkills.filter((_skill, index) =>\n personLower.has(requiredLower[index])\n );\n const missingSkills = requiredSkills.filter(\n (_skill, index) => !personLower.has(requiredLower[index])\n );\n return {\n id: person.id,\n name: person.name,\n matchPercent: round((matchedSkills.length / requiredSkills.length) * 100),\n matchedSkills,\n missingSkills,\n detail: person.detail,\n };\n })\n .filter((result) => result.matchedSkills.length > 0)\n .sort(\n (a, b) => b.matchPercent - a.matchPercent || b.matchedSkills.length - a.matchedSkills.length\n )\n .slice(0, limit);\n\n return { roleTitle: job.title, requiredSkills, matches, source };\n}\n\n// โ”€โ”€ Finance / payroll trends (best-effort) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n\nexport interface PayrollPeriodTotal {\n period: string;\n gross: number;\n net: number;\n deductions: number;\n}\n\n/**\n * Gross / net / deduction totals per payroll period, in first-seen period order\n * (period is a free-text label so it is not re-sorted). Empty when there are no\n * records.\n */\nexport function payrollTotals(payrollRecords: PayrollRecord[]): PayrollPeriodTotal[] {\n const order: string[] = [];\n const totals = new Map<string, PayrollPeriodTotal>();\n for (const record of payrollRecords) {\n let bucket = totals.get(record.period);\n if (!bucket) {\n bucket = { period: record.period, gross: 0, net: 0, deductions: 0 };\n totals.set(record.period, bucket);\n order.push(record.period);\n }\n bucket.gross += record.grossPay;\n bucket.net += record.netPay;\n bucket.deductions += record.deductions;\n }\n return order.map((period) => totals.get(period) as PayrollPeriodTotal);\n}\n\n/**\n * Monthly attrition: the count of employee exits per month, derived from the\n * real `Employee.terminationDate`, over the most recent `monthsWindow` months\n * that contain an exit (through the latest exit month, inclusive). Months with\n * no exits in that span read zero. Empty when no employee has a termination\n * date โ€” never a fabricated exit month.\n */\nexport function attritionTrend(employees: Employee[], monthsWindow = 12): LabeledValue[] {\n const exitIndices = employees\n .map((employee) => parseDate(employee.terminationDate))\n .filter((date): date is Date => date !== null)\n .map(monthIndex);\n\n if (exitIndices.length === 0) {\n return [];\n }\n\n const maxIndex = Math.max(...exitIndices);\n const minIndex = Math.max(Math.min(...exitIndices), maxIndex - (monthsWindow - 1));\n const includeYear = Math.floor(minIndex / 12) !== Math.floor(maxIndex / 12);\n\n const series: LabeledValue[] = [];\n for (let index = minIndex; index <= maxIndex; index += 1) {\n const count = exitIndices.filter((exit) => exit === index).length;\n series.push({ label: monthLabelFromIndex(index, includeYear), value: count });\n }\n return series;\n}\n\nexport interface TimeToHire {\n averageDays: number;\n sampleSize: number;\n}\n\n/**\n * Average time-to-hire: mean elapsed days from `appliedDate` to the real\n * `Candidate.hiredDate`, across candidates that carry both dates. Non-negative\n * spans only. Returns null (callers render an empty state) when no candidate\n * has a hired date โ€” the elapsed time is never estimated.\n */\nexport function timeToHire(candidates: Candidate[]): TimeToHire | null {\n let totalDays = 0;\n let sampleSize = 0;\n for (const candidate of candidates) {\n const hired = parseDate(candidate.hiredDate);\n const applied = parseDate(candidate.appliedDate);\n if (!hired || !applied) {\n continue;\n }\n const days = Math.round((hired.getTime() - applied.getTime()) / 86_400_000);\n if (days < 0) {\n continue;\n }\n totalDays += days;\n sampleSize += 1;\n }\n\n if (sampleSize === 0) {\n return null;\n }\n\n return { averageDays: Math.round(totalDays / sampleSize), sampleSize };\n}\n"],"mappings":"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"}
@@ -1,11 +1,7 @@
1
- import { Client, Invoice, Vendor } from '../store/crewfoundryStore';
1
+ import { Client, Vendor } from '../store/crewfoundryStore';
2
2
 
3
3
  /** Combined book of business: client revenue + vendor contract value. */
4
4
  export declare function totalContractValue(clients: Client[], vendors: Vendor[]): number;
5
- /**
6
- * Revenue invoiced in the current calendar quarter, from real invoice
7
- * `issueDate` values (cancelled invoices excluded). Returns 0 when no invoice
8
- * falls in the quarter โ€” never fabricated.
9
- */
10
- export declare function thisQuarterRevenue(invoices: Invoice[], now?: Date): number;
5
+ /** Revenue recorded against clients (non-churned clients only). */
6
+ export declare function clientRevenue(clients: Client[]): number;
11
7
  //# sourceMappingURL=companyFinance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"companyFinance.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/companyFinance.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEzE,yEAAyE;AACzE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAI/E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAqBtF"}
1
+ {"version":3,"file":"companyFinance.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/companyFinance.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEhE,yEAAyE;AACzE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAI/E;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAIvD"}
@@ -2,15 +2,10 @@
2
2
  function e(e, t) {
3
3
  return e.reduce((e, t) => e + (t.revenue || 0), 0) + t.reduce((e, t) => e + (t.contractValue || 0), 0);
4
4
  }
5
- function t(e, t = /* @__PURE__ */ new Date()) {
6
- let n = t.getUTCFullYear(), r = Math.floor(t.getUTCMonth() / 3) * 3;
7
- return e.filter((e) => e.status !== "cancelled").reduce((e, t) => {
8
- if (!t.issueDate) return e;
9
- let i = new Date(t.issueDate);
10
- return Number.isNaN(i.getTime()) ? e : i.getUTCFullYear() === n && i.getUTCMonth() >= r && i.getUTCMonth() < r + 3 ? e + t.amount : e;
11
- }, 0);
5
+ function t(e) {
6
+ return e.filter((e) => e.status !== "churned").reduce((e, t) => e + (t.revenue || 0), 0);
12
7
  }
13
8
  //#endregion
14
- export { t as thisQuarterRevenue, e as totalContractValue };
9
+ export { t as clientRevenue, e as totalContractValue };
15
10
 
16
11
  //# sourceMappingURL=companyFinance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"companyFinance.js","names":[],"sources":["../../../src/crewfoundry/lib/companyFinance.ts"],"sourcesContent":["/**\n * Pure company-finance helpers for the Clients & Vendors surface.\n *\n * Dependency-free transforms over REAL store entities โ€” no React, no network,\n * no fabrication. Used by the Client and Vendor management pages to compute the\n * shared KPI header (Total Contract Value, This Quarter revenue).\n */\n\nimport type { Client, Invoice, Vendor } from '../store/crewfoundryStore';\n\n/** Combined book of business: client revenue + vendor contract value. */\nexport function totalContractValue(clients: Client[], vendors: Vendor[]): number {\n const clientRevenue = clients.reduce((sum, client) => sum + (client.revenue || 0), 0);\n const vendorValue = vendors.reduce((sum, vendor) => sum + (vendor.contractValue || 0), 0);\n return clientRevenue + vendorValue;\n}\n\n/**\n * Revenue invoiced in the current calendar quarter, from real invoice\n * `issueDate` values (cancelled invoices excluded). Returns 0 when no invoice\n * falls in the quarter โ€” never fabricated.\n */\nexport function thisQuarterRevenue(invoices: Invoice[], now: Date = new Date()): number {\n const year = now.getUTCFullYear();\n const quarter = Math.floor(now.getUTCMonth() / 3);\n const startMonth = quarter * 3;\n\n return invoices\n .filter((invoice) => invoice.status !== 'cancelled')\n .reduce((sum, invoice) => {\n if (!invoice.issueDate) {\n return sum;\n }\n const date = new Date(invoice.issueDate);\n if (Number.isNaN(date.getTime())) {\n return sum;\n }\n const inQuarter =\n date.getUTCFullYear() === year &&\n date.getUTCMonth() >= startMonth &&\n date.getUTCMonth() < startMonth + 3;\n return inQuarter ? sum + invoice.amount : sum;\n }, 0);\n}\n"],"mappings":";AAWA,SAAgB,EAAmB,GAAmB,GAA2B;AAG/E,QAFsB,EAAQ,QAAQ,GAAK,MAAW,KAAO,EAAO,WAAW,IAAI,EAAE,GACjE,EAAQ,QAAQ,GAAK,MAAW,KAAO,EAAO,iBAAiB,IAAI,EAAE;;AAS3F,SAAgB,EAAmB,GAAqB,oBAAY,IAAI,MAAM,EAAU;CACtF,IAAM,IAAO,EAAI,gBAAgB,EAE3B,IADU,KAAK,MAAM,EAAI,aAAa,GAAG,EAAE,GACpB;AAE7B,QAAO,EACJ,QAAQ,MAAY,EAAQ,WAAW,YAAY,CACnD,QAAQ,GAAK,MAAY;AACxB,MAAI,CAAC,EAAQ,UACX,QAAO;EAET,IAAM,IAAO,IAAI,KAAK,EAAQ,UAAU;AAQxC,SAPI,OAAO,MAAM,EAAK,SAAS,CAAC,GACvB,IAGP,EAAK,gBAAgB,KAAK,KAC1B,EAAK,aAAa,IAAI,KACtB,EAAK,aAAa,GAAG,IAAa,IACjB,IAAM,EAAQ,SAAS;IACzC,EAAE"}
1
+ {"version":3,"file":"companyFinance.js","names":[],"sources":["../../../src/crewfoundry/lib/companyFinance.ts"],"sourcesContent":["/**\n * Pure company-finance helpers for the Clients & Vendors surface.\n *\n * Dependency-free transforms over REAL store entities โ€” no React, no network,\n * no fabrication. Used by the Client and Vendor management pages to compute the\n * shared KPI header (Total Contract Value, Client Revenue).\n */\n\nimport type { Client, Vendor } from '../store/crewfoundryStore';\n\n/** Combined book of business: client revenue + vendor contract value. */\nexport function totalContractValue(clients: Client[], vendors: Vendor[]): number {\n const clientRevenue = clients.reduce((sum, client) => sum + (client.revenue || 0), 0);\n const vendorValue = vendors.reduce((sum, vendor) => sum + (vendor.contractValue || 0), 0);\n return clientRevenue + vendorValue;\n}\n\n/** Revenue recorded against clients (non-churned clients only). */\nexport function clientRevenue(clients: Client[]): number {\n return clients\n .filter((client) => client.status !== 'churned')\n .reduce((sum, client) => sum + (client.revenue || 0), 0);\n}\n"],"mappings":";AAWA,SAAgB,EAAmB,GAAmB,GAA2B;AAG/E,QAFsB,EAAQ,QAAQ,GAAK,MAAW,KAAO,EAAO,WAAW,IAAI,EAAE,GACjE,EAAQ,QAAQ,GAAK,MAAW,KAAO,EAAO,iBAAiB,IAAI,EAAE;;AAK3F,SAAgB,EAAc,GAA2B;AACvD,QAAO,EACJ,QAAQ,MAAW,EAAO,WAAW,UAAU,CAC/C,QAAQ,GAAK,MAAW,KAAO,EAAO,WAAW,IAAI,EAAE"}
@@ -0,0 +1,12 @@
1
+ import { CrewEnrollmentStatus } from '../store/crewfoundryStore';
2
+
3
+ export declare const ENROLLMENT_STATUS_LABEL: Record<CrewEnrollmentStatus, string>;
4
+ export declare const ENROLLMENT_STATUS_OPTIONS: {
5
+ value: CrewEnrollmentStatus;
6
+ label: string;
7
+ }[];
8
+ /** "45 min", "2 h", "1 h 30 min"; "Self-paced" when no duration is recorded. */
9
+ export declare function formatDuration(minutes: number | null | undefined): string;
10
+ /** Only http(s) links are accepted as course content (no javascript:/data: URLs). */
11
+ export declare function isSafeContentUrl(value: string): boolean;
12
+ //# sourceMappingURL=learning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learning.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/learning.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAIxE,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;GAE8B,CAAC;AAErE,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAMzE;AAED,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOvD"}