@burdenoff/microfe-crewfoundry 2026.704.6 → 2026.704.7
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.
- package/dist/crewfoundry/CrewFoundryRoutes.js +51 -43
- package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
- package/dist/crewfoundry/components/forms/formValues.js +5 -2
- package/dist/crewfoundry/components/forms/formValues.js.map +1 -1
- package/dist/crewfoundry/components/viz/AllocationBar.js +68 -0
- package/dist/crewfoundry/components/viz/AllocationBar.js.map +1 -0
- package/dist/crewfoundry/components/viz/AreaChart.js +102 -0
- package/dist/crewfoundry/components/viz/AreaChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/AvatarStack.js +36 -0
- package/dist/crewfoundry/components/viz/AvatarStack.js.map +1 -0
- package/dist/crewfoundry/components/viz/BarChart.js +83 -0
- package/dist/crewfoundry/components/viz/BarChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/CertificateCard.js +118 -0
- package/dist/crewfoundry/components/viz/CertificateCard.js.map +1 -0
- package/dist/crewfoundry/components/viz/DonutChart.js +109 -0
- package/dist/crewfoundry/components/viz/DonutChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/HBarFunnel.js +54 -0
- package/dist/crewfoundry/components/viz/HBarFunnel.js.map +1 -0
- package/dist/crewfoundry/components/viz/Heatmap.js +91 -0
- package/dist/crewfoundry/components/viz/Heatmap.js.map +1 -0
- package/dist/crewfoundry/components/viz/KanbanBoard.js +50 -0
- package/dist/crewfoundry/components/viz/KanbanBoard.js.map +1 -0
- package/dist/crewfoundry/components/viz/LineChart.js +123 -0
- package/dist/crewfoundry/components/viz/LineChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/OrgTree.js +83 -0
- package/dist/crewfoundry/components/viz/OrgTree.js.map +1 -0
- package/dist/crewfoundry/components/viz/PathSteps.js +68 -0
- package/dist/crewfoundry/components/viz/PathSteps.js.map +1 -0
- package/dist/crewfoundry/components/viz/ProgressRing.js +68 -0
- package/dist/crewfoundry/components/viz/ProgressRing.js.map +1 -0
- package/dist/crewfoundry/components/viz/RatingStars.js +51 -0
- package/dist/crewfoundry/components/viz/RatingStars.js.map +1 -0
- package/dist/crewfoundry/components/viz/SegmentedTabs.js +69 -0
- package/dist/crewfoundry/components/viz/SegmentedTabs.js.map +1 -0
- package/dist/crewfoundry/components/viz/SkillBar.js +3 -0
- package/dist/crewfoundry/components/viz/Sparkline.js +74 -0
- package/dist/crewfoundry/components/viz/Sparkline.js.map +1 -0
- package/dist/crewfoundry/components/viz/StatCard.js +60 -0
- package/dist/crewfoundry/components/viz/StatCard.js.map +1 -0
- package/dist/crewfoundry/components/viz/StepProgress.js +3 -0
- package/dist/crewfoundry/components/viz/Timeline.js +66 -0
- package/dist/crewfoundry/components/viz/Timeline.js.map +1 -0
- package/dist/crewfoundry/components/viz/index.js +23 -0
- package/dist/crewfoundry/components/viz/lib/Avatar.js +30 -0
- package/dist/crewfoundry/components/viz/lib/Avatar.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/CartesianFrame.js +119 -0
- package/dist/crewfoundry/components/viz/lib/CartesianFrame.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/ResponsiveSvg.js +35 -0
- package/dist/crewfoundry/components/viz/lib/ResponsiveSvg.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/format.js +21 -0
- package/dist/crewfoundry/components/viz/lib/format.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/geometry.js +69 -0
- package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/hooks.js +39 -0
- package/dist/crewfoundry/components/viz/lib/hooks.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/palette.js +27 -0
- package/dist/crewfoundry/components/viz/lib/palette.js.map +1 -0
- package/dist/crewfoundry/lib/analytics.js +323 -0
- package/dist/crewfoundry/lib/analytics.js.map +1 -0
- package/dist/crewfoundry/lib/companyFinance.js +16 -0
- package/dist/crewfoundry/lib/companyFinance.js.map +1 -0
- package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js +226 -283
- package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js +107 -120
- package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js +122 -162
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js +121 -158
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +316 -211
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +75 -27
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +167 -165
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +168 -109
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +401 -290
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +84 -75
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +368 -174
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +236 -201
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js +369 -196
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +269 -270
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +210 -110
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +216 -178
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +385 -0
- package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -0
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js +189 -130
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js +397 -241
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrPayrollPagePage.js +262 -147
- package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js +345 -167
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +98 -24
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +289 -108
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js +221 -114
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +134 -135
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +148 -97
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +264 -157
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +85 -93
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +211 -122
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +94 -54
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +289 -140
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js +169 -79
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +296 -125
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +187 -0
- package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -0
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +140 -78
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +93 -43
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +250 -98
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
- package/dist/crewfoundry/utils/cn.js +10 -0
- package/dist/crewfoundry/utils/cn.js.map +1 -0
- package/dist/crewfoundry/utils/courseProgress.js +46 -0
- package/dist/crewfoundry/utils/courseProgress.js.map +1 -0
- package/dist/node_modules/clsx/dist/clsx.js +18 -0
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1716 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","names":[],"sources":["../../../../../src/crewfoundry/components/viz/lib/hooks.ts"],"sourcesContent":["import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nconst canUseDOM = typeof window !== 'undefined';\nconst useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;\n\n/**\n * Tracks the user's reduced-motion preference. Components use this to skip\n * entrance / draw-in transitions entirely rather than merely shortening them.\n */\nexport function usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n if (!canUseDOM || typeof window.matchMedia !== 'function') {\n return;\n }\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n const update = () => setReduced(mq.matches);\n update();\n mq.addEventListener('change', update);\n return () => mq.removeEventListener('change', update);\n }, []);\n return reduced;\n}\n\n/** Becomes true after the first paint — drives one-shot enter transitions. */\nexport function useMounted(): boolean {\n const [mounted, setMounted] = useState(false);\n useEffect(() => {\n const raf = requestAnimationFrame(() => setMounted(true));\n return () => cancelAnimationFrame(raf);\n }, []);\n return mounted;\n}\n\nlet uidCounter = 0;\n\n/** Stable, SVG-safe unique id (no colons) for gradient / clip / aria references. */\nexport function useVizId(prefix = 'viz'): string {\n const [id] = useState(() => {\n uidCounter += 1;\n return `${prefix}-${uidCounter}`;\n });\n return id;\n}\n\n/**\n * Observes the width of an element so SVG charts can render a 1:1 viewBox\n * (no aspect-ratio distortion) and reflow on resize. Falls back gracefully\n * where ResizeObserver is unavailable.\n */\nexport function useElementWidth<T extends HTMLElement>(): [RefObject<T | null>, number] {\n const ref = useRef<T>(null);\n const [width, setWidth] = useState(0);\n useIsomorphicLayoutEffect(() => {\n const el = ref.current;\n if (!el) {\n return;\n }\n setWidth(el.clientWidth);\n if (typeof ResizeObserver === 'undefined') {\n return;\n }\n const ro = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (typeof measured === 'number') {\n setWidth(measured);\n }\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n return [ref, width];\n}\n"],"mappings":";;AAEA,IAAM,IAAY,OAAO,SAAW,KAC9B,IAA4B,IAAY,IAAkB;AAMhE,SAAgB,IAAmC;CACjD,IAAM,CAAC,GAAS,KAAc,EAAS,GAAM;AAW7C,QAVA,QAAgB;AACd,MAAI,CAAC,KAAa,OAAO,OAAO,cAAe,WAC7C;EAEF,IAAM,IAAK,OAAO,WAAW,mCAAmC,EAC1D,UAAe,EAAW,EAAG,QAAQ;AAG3C,SAFA,GAAQ,EACR,EAAG,iBAAiB,UAAU,EAAO,QACxB,EAAG,oBAAoB,UAAU,EAAO;IACpD,EAAE,CAAC,EACC;;AAIT,SAAgB,IAAsB;CACpC,IAAM,CAAC,GAAS,KAAc,EAAS,GAAM;AAK7C,QAJA,QAAgB;EACd,IAAM,IAAM,4BAA4B,EAAW,GAAK,CAAC;AACzD,eAAa,qBAAqB,EAAI;IACrC,EAAE,CAAC,EACC;;AAGT,IAAI,IAAa;AAGjB,SAAgB,EAAS,IAAS,OAAe;CAC/C,IAAM,CAAC,KAAM,SACX,KAAc,GACP,GAAG,EAAO,GAAG,KACpB;AACF,QAAO;;AAQT,SAAgB,IAAwE;CACtF,IAAM,IAAM,EAAU,KAAK,EACrB,CAAC,GAAO,KAAY,EAAS,EAAE;AAmBrC,QAlBA,QAAgC;EAC9B,IAAM,IAAK,EAAI;AAKf,MAJI,CAAC,MAGL,EAAS,EAAG,YAAY,EACpB,OAAO,iBAAmB,KAC5B;EAEF,IAAM,IAAK,IAAI,gBAAgB,MAAY;GACzC,IAAM,IAAW,EAAQ,IAAI,YAAY;AACzC,GAAI,OAAO,KAAa,YACtB,EAAS,EAAS;IAEpB;AAEF,SADA,EAAG,QAAQ,EAAG,QACD,EAAG,YAAY;IAC3B,EAAE,CAAC,EACC,CAAC,GAAK,EAAM"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/crewfoundry/components/viz/lib/palette.ts
|
|
2
|
+
var e = "var(--color-accent-primary)", t = "var(--color-chart-grid)", n = "var(--color-chart-axis)", r = {
|
|
3
|
+
success: "var(--color-status-success-text)",
|
|
4
|
+
warning: "var(--color-status-warning-text)",
|
|
5
|
+
danger: "var(--color-status-error-text)",
|
|
6
|
+
info: "var(--color-status-info-text)"
|
|
7
|
+
}, i = [
|
|
8
|
+
e,
|
|
9
|
+
"var(--color-accent-blue)",
|
|
10
|
+
"var(--color-accent-teal)",
|
|
11
|
+
"var(--color-accent-purple)",
|
|
12
|
+
"var(--color-accent-indigo)",
|
|
13
|
+
r.warning,
|
|
14
|
+
r.info,
|
|
15
|
+
r.success,
|
|
16
|
+
"var(--color-chart-6)",
|
|
17
|
+
"var(--color-chart-7)",
|
|
18
|
+
"var(--color-chart-9)",
|
|
19
|
+
"var(--color-chart-12)"
|
|
20
|
+
];
|
|
21
|
+
function a(e) {
|
|
22
|
+
return e <= 0 ? [] : Array.from({ length: e }, (e, t) => i[t % i.length]);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { e as ACCENT, n as CHART_AXIS, t as CHART_GRID, r as STATUS, a as chartPalette };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=palette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"palette.js","names":[],"sources":["../../../../../src/crewfoundry/components/viz/lib/palette.ts"],"sourcesContent":["/**\n * Tokenized color palette for the visualization kit.\n *\n * Every color here is a reference to a fe-libs semantic CSS variable, so the\n * kit themes automatically (light / dark / product themes) and never ships a\n * raw hex. The single \"accent\" used for highlights and single-series charts is\n * the product theme's primary action color — the app is NOT green; whatever the\n * active theme resolves `--color-accent-primary` to is what renders.\n *\n * Tints/shades are produced by SVG opacity (fill-opacity / stop-opacity) at the\n * call site, never by emitting a second raw color.\n */\n\n/** Primary highlight — resolves to the product theme's primary action color. */\nexport const ACCENT = 'var(--color-accent-primary)';\n\n/** Faint chart chrome (theme-aware). */\nexport const CHART_GRID = 'var(--color-chart-grid)';\nexport const CHART_AXIS = 'var(--color-chart-axis)';\n\n/** Semantic status hues — saturated and readable in both light and dark. */\nexport const STATUS = {\n success: 'var(--color-status-success-text)',\n warning: 'var(--color-status-warning-text)',\n danger: 'var(--color-status-error-text)',\n info: 'var(--color-status-info-text)',\n} as const;\n\nexport type StatusTone = keyof typeof STATUS;\n\n/**\n * Ordered categorical palette, led by the product accent and drawn entirely\n * from theme-aware semantic tokens. Used to color multi-series charts.\n */\nconst SERIES_TOKENS: readonly string[] = [\n ACCENT,\n 'var(--color-accent-blue)',\n 'var(--color-accent-teal)',\n 'var(--color-accent-purple)',\n 'var(--color-accent-indigo)',\n STATUS.warning,\n STATUS.info,\n STATUS.success,\n 'var(--color-chart-6)',\n 'var(--color-chart-7)',\n 'var(--color-chart-9)',\n 'var(--color-chart-12)',\n];\n\n/**\n * A tokenized palette of `count` colors, cycling the semantic series list.\n * Returns CSS-variable strings (never raw colors).\n */\nexport function chartPalette(count: number): string[] {\n if (count <= 0) {\n return [];\n }\n return Array.from({ length: count }, (_, i) => SERIES_TOKENS[i % SERIES_TOKENS.length]);\n}\n"],"mappings":";AAcA,IAAa,IAAS,+BAGT,IAAa,2BACb,IAAa,2BAGb,IAAS;CACpB,SAAS;CACT,SAAS;CACT,QAAQ;CACR,MAAM;CACP,EAQK,IAAmC;CACvC;CACA;CACA;CACA;CACA;CACA,EAAO;CACP,EAAO;CACP,EAAO;CACP;CACA;CACA;CACA;CACD;AAMD,SAAgB,EAAa,GAAyB;AAIpD,QAHI,KAAS,IACJ,EAAE,GAEJ,MAAM,KAAK,EAAE,QAAQ,GAAO,GAAG,GAAG,MAAM,EAAc,IAAI,EAAc,QAAQ"}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { CHART_GRID as e, STATUS as t } from "../components/viz/lib/palette.js";
|
|
2
|
+
//#region src/crewfoundry/lib/analytics.ts
|
|
3
|
+
var n = [
|
|
4
|
+
"Jan",
|
|
5
|
+
"Feb",
|
|
6
|
+
"Mar",
|
|
7
|
+
"Apr",
|
|
8
|
+
"May",
|
|
9
|
+
"Jun",
|
|
10
|
+
"Jul",
|
|
11
|
+
"Aug",
|
|
12
|
+
"Sep",
|
|
13
|
+
"Oct",
|
|
14
|
+
"Nov",
|
|
15
|
+
"Dec"
|
|
16
|
+
];
|
|
17
|
+
function r(e) {
|
|
18
|
+
if (!e) return null;
|
|
19
|
+
let t = new Date(e);
|
|
20
|
+
return Number.isNaN(t.getTime()) ? null : t;
|
|
21
|
+
}
|
|
22
|
+
function i(e) {
|
|
23
|
+
return e.getUTCFullYear() * 12 + e.getUTCMonth();
|
|
24
|
+
}
|
|
25
|
+
function a(e, t) {
|
|
26
|
+
let r = Math.floor(e / 12), i = n[e % 12];
|
|
27
|
+
return t ? `${i} ${String(r).slice(2)}` : i;
|
|
28
|
+
}
|
|
29
|
+
function o(e) {
|
|
30
|
+
return Math.round(e);
|
|
31
|
+
}
|
|
32
|
+
function s(e) {
|
|
33
|
+
let t = new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate())), n = t.getUTCDay() || 7;
|
|
34
|
+
t.setUTCDate(t.getUTCDate() + 4 - n);
|
|
35
|
+
let r = new Date(Date.UTC(t.getUTCFullYear(), 0, 1)), i = Math.ceil(((t.getTime() - r.getTime()) / 864e5 + 1) / 7);
|
|
36
|
+
return {
|
|
37
|
+
year: t.getUTCFullYear(),
|
|
38
|
+
week: i
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function c(e) {
|
|
42
|
+
let { year: t, week: n } = s(e);
|
|
43
|
+
return `${t}-W${String(n).padStart(2, "0")}`;
|
|
44
|
+
}
|
|
45
|
+
function l(e) {
|
|
46
|
+
let t = /^(\d{4})-W(\d{2})$/.exec(e);
|
|
47
|
+
if (!t) return null;
|
|
48
|
+
let n = Number(t[1]), r = Number(t[2]), i = new Date(Date.UTC(n, 0, 4)), a = i.getUTCDay() || 7, o = new Date(i);
|
|
49
|
+
o.setUTCDate(i.getUTCDate() - (a - 1));
|
|
50
|
+
let s = new Date(o);
|
|
51
|
+
return s.setUTCDate(o.getUTCDate() + (r - 1) * 7), s;
|
|
52
|
+
}
|
|
53
|
+
function u(e) {
|
|
54
|
+
let t = l(e);
|
|
55
|
+
if (!t) return e;
|
|
56
|
+
let r = new Date(t);
|
|
57
|
+
r.setUTCDate(t.getUTCDate() + 6);
|
|
58
|
+
let i = n[t.getUTCMonth()], a = n[r.getUTCMonth()];
|
|
59
|
+
return t.getUTCMonth() === r.getUTCMonth() ? `${i} ${t.getUTCDate()}–${r.getUTCDate()}` : `${i} ${t.getUTCDate()} – ${a} ${r.getUTCDate()}`;
|
|
60
|
+
}
|
|
61
|
+
var d = 40;
|
|
62
|
+
function f(e) {
|
|
63
|
+
return `${e.firstName} ${e.lastName}`.trim();
|
|
64
|
+
}
|
|
65
|
+
function p(e) {
|
|
66
|
+
let t = e.map((e) => r(e.startDate)).filter((e) => e !== null).map(i);
|
|
67
|
+
if (t.length === 0) return [];
|
|
68
|
+
let n = Math.min(...t), o = Math.max(...t), s = Math.floor(n / 12) !== Math.floor(o / 12), c = [];
|
|
69
|
+
for (let e = n; e <= o; e += 1) {
|
|
70
|
+
let n = t.filter((t) => t <= e).length;
|
|
71
|
+
c.push({
|
|
72
|
+
label: a(e, s),
|
|
73
|
+
value: n
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
function m(e) {
|
|
79
|
+
if (e.length === 0) return {
|
|
80
|
+
current: 0,
|
|
81
|
+
previous: 0,
|
|
82
|
+
delta: 0,
|
|
83
|
+
deltaPercent: 0
|
|
84
|
+
};
|
|
85
|
+
let t = e[e.length - 1].value, n = e.length > 1 ? e[e.length - 2].value : t, r = t - n;
|
|
86
|
+
return {
|
|
87
|
+
current: t,
|
|
88
|
+
previous: n,
|
|
89
|
+
delta: r,
|
|
90
|
+
deltaPercent: n > 0 ? o(r / n * 100) : 0
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function h(e) {
|
|
94
|
+
let t = e.map((e) => e.performanceScore).filter((e) => typeof e == "number" && !Number.isNaN(e));
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
label: "< 2.5",
|
|
98
|
+
value: t.filter((e) => e < 2.5).length
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "2.5–3.5",
|
|
102
|
+
value: t.filter((e) => e >= 2.5 && e < 3.5).length
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: "3.5–4.5",
|
|
106
|
+
value: t.filter((e) => e >= 3.5 && e < 4.5).length
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: "4.5+",
|
|
110
|
+
value: t.filter((e) => e >= 4.5).length
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
function g(e) {
|
|
115
|
+
return e.map((e) => ({
|
|
116
|
+
label: e.name,
|
|
117
|
+
value: e.employeeCount
|
|
118
|
+
})).sort((e, t) => t.value - e.value);
|
|
119
|
+
}
|
|
120
|
+
function _(e, t, n) {
|
|
121
|
+
let i = new Map(t.map((e) => [e.id, e])), a = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Set();
|
|
122
|
+
for (let t of e) {
|
|
123
|
+
let e = r(t.date);
|
|
124
|
+
if (!e || typeof t.totalHours != "number" || Number.isNaN(t.totalHours)) continue;
|
|
125
|
+
let n = c(e);
|
|
126
|
+
s.add(n);
|
|
127
|
+
let i = a.get(t.employeeId);
|
|
128
|
+
i || (i = /* @__PURE__ */ new Map(), a.set(t.employeeId, i)), i.set(n, (i.get(n) ?? 0) + t.totalHours);
|
|
129
|
+
}
|
|
130
|
+
let l = [...s].sort(), p = typeof n == "number" && n > 0 ? l.slice(-n) : l;
|
|
131
|
+
if (p.length === 0 || a.size === 0) return {
|
|
132
|
+
rows: [],
|
|
133
|
+
weekKeys: [],
|
|
134
|
+
weekLabels: [],
|
|
135
|
+
weekRanges: [],
|
|
136
|
+
personUtil: [],
|
|
137
|
+
hasData: !1
|
|
138
|
+
};
|
|
139
|
+
let m = p.map((e, t) => `W${t + 1}`), h = p.map((e) => u(e)), g = [];
|
|
140
|
+
for (let [t, n] of a) {
|
|
141
|
+
let r = p.map((e) => {
|
|
142
|
+
let t = n.get(e);
|
|
143
|
+
return t === void 0 ? null : o(t / d * 100);
|
|
144
|
+
}), a = r.filter((e) => e !== null);
|
|
145
|
+
if (a.length === 0) continue;
|
|
146
|
+
let s = o(a.reduce((e, t) => e + t, 0) / a.length), c = a[a.length - 1];
|
|
147
|
+
for (let e = r.length - 1; e >= 0; --e) {
|
|
148
|
+
let t = r[e];
|
|
149
|
+
if (t !== null) {
|
|
150
|
+
c = t;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let l = i.get(t), u = l ? f(l) : e.find((e) => e.employeeId === t)?.employeeName ?? "Unknown";
|
|
155
|
+
g.push({
|
|
156
|
+
employeeId: t,
|
|
157
|
+
name: u,
|
|
158
|
+
weekly: r,
|
|
159
|
+
avg: s,
|
|
160
|
+
latest: c
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
g.sort((e, t) => t.avg - e.avg);
|
|
164
|
+
let _ = g.map((e) => ({
|
|
165
|
+
label: e.name,
|
|
166
|
+
cells: e.weekly.map((e) => ({ value: e ?? 0 }))
|
|
167
|
+
}));
|
|
168
|
+
return {
|
|
169
|
+
rows: _,
|
|
170
|
+
weekKeys: p,
|
|
171
|
+
weekLabels: m,
|
|
172
|
+
weekRanges: h,
|
|
173
|
+
personUtil: g,
|
|
174
|
+
hasData: _.length > 0
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function v(e, t) {
|
|
178
|
+
let n = new Set(e.personUtil.map((e) => e.employeeId)), r = t.filter((e) => e.status === "active").filter((e) => !n.has(e.id)).length, i = e.personUtil.filter((e) => e.latest > 100).length;
|
|
179
|
+
return {
|
|
180
|
+
avgUtilization: e.personUtil.length > 0 ? o(e.personUtil.reduce((e, t) => e + t.avg, 0) / e.personUtil.length) : 0,
|
|
181
|
+
bench: r,
|
|
182
|
+
overAllocated: i,
|
|
183
|
+
allocatedCount: e.personUtil.length
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function y(n) {
|
|
187
|
+
let r = 0, i = 0;
|
|
188
|
+
for (let e of n) {
|
|
189
|
+
let t = typeof e.totalHours == "number" && !Number.isNaN(e.totalHours) ? e.totalHours : 0;
|
|
190
|
+
e.billable ? r += t : i += t;
|
|
191
|
+
}
|
|
192
|
+
return r + i <= 0 ? null : [{
|
|
193
|
+
label: "Billable",
|
|
194
|
+
value: r,
|
|
195
|
+
color: t.success
|
|
196
|
+
}, {
|
|
197
|
+
label: "Non-billable",
|
|
198
|
+
value: i,
|
|
199
|
+
color: e
|
|
200
|
+
}];
|
|
201
|
+
}
|
|
202
|
+
function b(e, t) {
|
|
203
|
+
let n = e.map((e) => ({
|
|
204
|
+
entry: e,
|
|
205
|
+
date: r(e.date)
|
|
206
|
+
})).filter((e) => e.date !== null && typeof e.entry.totalHours == "number" && !Number.isNaN(e.entry.totalHours));
|
|
207
|
+
if (n.length === 0) return [];
|
|
208
|
+
let i = n.map((e) => c(e.date)).sort().slice(-1)[0], a = u(i), s = new Map(t.map((e) => [e.id, e])), l = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map();
|
|
209
|
+
for (let { entry: e, date: t } of n) {
|
|
210
|
+
if (c(t) !== i) continue;
|
|
211
|
+
let n = l.get(e.employeeId);
|
|
212
|
+
n || (n = /* @__PURE__ */ new Map(), l.set(e.employeeId, n));
|
|
213
|
+
let r = e.project || "Unassigned";
|
|
214
|
+
n.set(r, (n.get(r) ?? 0) + e.totalHours);
|
|
215
|
+
let a = s.get(e.employeeId);
|
|
216
|
+
p.set(e.employeeId, a ? f(a) : e.employeeName || "Unknown");
|
|
217
|
+
}
|
|
218
|
+
let m = [];
|
|
219
|
+
for (let [e, t] of l) {
|
|
220
|
+
let n = [...t.entries()].map(([e, t]) => ({
|
|
221
|
+
label: e,
|
|
222
|
+
value: t
|
|
223
|
+
})), r = n.reduce((e, t) => e + t.value, 0);
|
|
224
|
+
m.push({
|
|
225
|
+
employeeId: e,
|
|
226
|
+
name: p.get(e) ?? "Unknown",
|
|
227
|
+
segments: n,
|
|
228
|
+
utilization: o(r / d * 100),
|
|
229
|
+
weekLabel: a
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return m.sort((e, t) => t.utilization - e.utilization), m;
|
|
233
|
+
}
|
|
234
|
+
function x(e) {
|
|
235
|
+
let t = e.experience;
|
|
236
|
+
return `${`${t} yr${t === 1 ? "" : "s"} experience`} · ${e.status.replace(/_/g, " ")}`;
|
|
237
|
+
}
|
|
238
|
+
function S(e, t, n, r = 6) {
|
|
239
|
+
let i = t.filter((e) => e.skills.length > 0).map((e) => ({
|
|
240
|
+
id: e.id,
|
|
241
|
+
name: `${e.firstName} ${e.lastName}`.trim(),
|
|
242
|
+
skills: e.skills,
|
|
243
|
+
detail: [e.position, e.department].filter(Boolean).join(" · ")
|
|
244
|
+
})), a = i.length > 0 ? "employee" : "candidate", s = a === "employee" ? i : n.map((e) => ({
|
|
245
|
+
id: e.id,
|
|
246
|
+
name: `${e.firstName} ${e.lastName}`.trim(),
|
|
247
|
+
skills: e.skills,
|
|
248
|
+
detail: x(e)
|
|
249
|
+
})), c = /* @__PURE__ */ new Map();
|
|
250
|
+
for (let e of s) for (let t of e.skills) {
|
|
251
|
+
let e = t.trim();
|
|
252
|
+
e.length >= 2 && !c.has(e.toLowerCase()) && c.set(e.toLowerCase(), e);
|
|
253
|
+
}
|
|
254
|
+
let l = `${e.title} ${e.requirements.join(" ")}`.toLowerCase(), u = [...c.entries()].filter(([e]) => l.includes(e)).map(([, e]) => e);
|
|
255
|
+
if (u.length === 0) return {
|
|
256
|
+
roleTitle: e.title,
|
|
257
|
+
requiredSkills: [],
|
|
258
|
+
matches: [],
|
|
259
|
+
source: a
|
|
260
|
+
};
|
|
261
|
+
let d = u.map((e) => e.toLowerCase()), f = s.map((e) => {
|
|
262
|
+
let t = new Set(e.skills.map((e) => e.toLowerCase())), n = u.filter((e, n) => t.has(d[n])), r = u.filter((e, n) => !t.has(d[n]));
|
|
263
|
+
return {
|
|
264
|
+
id: e.id,
|
|
265
|
+
name: e.name,
|
|
266
|
+
matchPercent: o(n.length / u.length * 100),
|
|
267
|
+
matchedSkills: n,
|
|
268
|
+
missingSkills: r,
|
|
269
|
+
detail: e.detail
|
|
270
|
+
};
|
|
271
|
+
}).filter((e) => e.matchedSkills.length > 0).sort((e, t) => t.matchPercent - e.matchPercent || t.matchedSkills.length - e.matchedSkills.length).slice(0, r);
|
|
272
|
+
return {
|
|
273
|
+
roleTitle: e.title,
|
|
274
|
+
requiredSkills: u,
|
|
275
|
+
matches: f,
|
|
276
|
+
source: a
|
|
277
|
+
};
|
|
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) {
|
|
295
|
+
let n = e.map((e) => r(e.terminationDate)).filter((e) => e !== null).map(i);
|
|
296
|
+
if (n.length === 0) return [];
|
|
297
|
+
let o = Math.max(...n), s = Math.max(Math.min(...n), o - (t - 1)), c = Math.floor(s / 12) !== Math.floor(o / 12), l = [];
|
|
298
|
+
for (let e = s; e <= o; e += 1) {
|
|
299
|
+
let t = n.filter((t) => t === e).length;
|
|
300
|
+
l.push({
|
|
301
|
+
label: a(e, c),
|
|
302
|
+
value: t
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return l;
|
|
306
|
+
}
|
|
307
|
+
function T(e) {
|
|
308
|
+
let t = 0, n = 0;
|
|
309
|
+
for (let i of e) {
|
|
310
|
+
let e = r(i.hiredDate), a = r(i.appliedDate);
|
|
311
|
+
if (!e || !a) continue;
|
|
312
|
+
let o = Math.round((e.getTime() - a.getTime()) / 864e5);
|
|
313
|
+
o < 0 || (t += o, n += 1);
|
|
314
|
+
}
|
|
315
|
+
return n === 0 ? null : {
|
|
316
|
+
averageDays: Math.round(t / n),
|
|
317
|
+
sampleSize: n
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
//#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 };
|
|
322
|
+
|
|
323
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +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.map((item) => isoWeekKey(item.date)).sort().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((a, b) => b.matchPercent - a.matchPercent || b.matchedSkills.length - a.matchedSkills.length)\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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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/crewfoundry/lib/companyFinance.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
return e.reduce((e, t) => e + (t.revenue || 0), 0) + t.reduce((e, t) => e + (t.contractValue || 0), 0);
|
|
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);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { t as thisQuarterRevenue, e as totalContractValue };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=companyFinance.js.map
|
|
@@ -0,0 +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"}
|