@burdenoff/microfe-crewfoundry 2026.1029.2 → 2026.1030.2

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 (47) hide show
  1. package/dist/crewfoundry/components/EmployeeSkillsPanel.d.ts +12 -0
  2. package/dist/crewfoundry/components/EmployeeSkillsPanel.d.ts.map +1 -0
  3. package/dist/crewfoundry/components/EmployeeSkillsPanel.js +168 -0
  4. package/dist/crewfoundry/components/EmployeeSkillsPanel.js.map +1 -0
  5. package/dist/crewfoundry/lib/useFocusedRecordParam.d.ts +51 -0
  6. package/dist/crewfoundry/lib/useFocusedRecordParam.d.ts.map +1 -0
  7. package/dist/crewfoundry/lib/useFocusedRecordParam.js +38 -0
  8. package/dist/crewfoundry/lib/useFocusedRecordParam.js.map +1 -0
  9. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.d.ts.map +1 -1
  10. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +119 -112
  11. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
  12. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.d.ts.map +1 -1
  13. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +78 -71
  14. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  15. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.d.ts.map +1 -1
  16. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +88 -81
  17. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
  18. package/dist/crewfoundry/pages/HiringCandidatesPagePage.d.ts.map +1 -1
  19. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +133 -126
  20. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/HiringInterviewsPagePage.d.ts.map +1 -1
  22. package/dist/crewfoundry/pages/HiringInterviewsPagePage.js +150 -143
  23. package/dist/crewfoundry/pages/HiringInterviewsPagePage.js.map +1 -1
  24. package/dist/crewfoundry/pages/HrDepartmentsPagePage.d.ts.map +1 -1
  25. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +110 -103
  26. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.d.ts.map +1 -1
  28. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +124 -134
  29. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -1
  30. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.d.ts.map +1 -1
  31. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js +173 -162
  32. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
  33. package/dist/crewfoundry/pages/HrPayrollPagePage.d.ts.map +1 -1
  34. package/dist/crewfoundry/pages/HrPayrollPagePage.js +169 -158
  35. package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
  36. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.d.ts.map +1 -1
  37. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js +179 -163
  38. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
  39. package/dist/crewfoundry/store/crewfoundryStore.d.ts +22 -0
  40. package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
  41. package/dist/crewfoundry/store/crewfoundryStore.js +158 -129
  42. package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
  43. package/dist/utils/pageContext.d.ts +43 -4
  44. package/dist/utils/pageContext.d.ts.map +1 -1
  45. package/dist/utils/pageContext.js +10 -3
  46. package/dist/utils/pageContext.js.map +1 -1
  47. package/package.json +3 -2
@@ -0,0 +1,12 @@
1
+ export interface EmployeeSkillsPanelProps {
2
+ employeeId: string;
3
+ }
4
+ /**
5
+ * Employee profile Skills tab (BOFF-3717, mock cf-03). Shows the employee's
6
+ * canonical skills from the skills taxonomy with real proficiency bars.
7
+ * Skills without an assessed proficiency (for example ones backfilled from
8
+ * the old free-text tags) say so instead of showing an invented level.
9
+ * Typed names resolve through taxonomy aliases, so "ReactJS" lands on React.
10
+ */
11
+ export declare function EmployeeSkillsPanel({ employeeId }: EmployeeSkillsPanelProps): import("react").JSX.Element;
12
+ //# sourceMappingURL=EmployeeSkillsPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmployeeSkillsPanel.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/components/EmployeeSkillsPanel.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,EAAE,wBAAwB,+BAiL3E"}
@@ -0,0 +1,168 @@
1
+ import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
2
+ import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
3
+ import { SkillBar as n } from "./viz/SkillBar.js";
4
+ import "./viz/index.js";
5
+ import { useEffect as r, useRef as i, useState as a } from "react";
6
+ import { Plus as o, X as s } from "lucide-react";
7
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
8
+ import { Badge as u, Button as d, IllustratedEmptyState as f, Input as p, Label as m, Spinner as h } from "@burdenoff/fe-libs/ui";
9
+ import { useGlobalUi as g } from "@burdenoff/fe-libs/shared/providers/shell";
10
+ //#region src/crewfoundry/components/EmployeeSkillsPanel.tsx
11
+ var _ = [];
12
+ function v({ employeeId: v }) {
13
+ let { showToast: y } = g(), b = e((e) => e.employeeSkillProfiles[v]), x = e((e) => e.fetchEmployeeSkillProfile), S = e((e) => e.attachEmployeeSkill), C = e((e) => e.detachEmployeeSkill), [w, T] = a(""), [E, D] = a(""), [O, k] = a(!1), [A, j] = a(null), [M, N] = a(!1), P = i(null);
14
+ r(() => {
15
+ P.current !== v && (P.current = v, x(v).catch((e) => {
16
+ N(!0), y(e instanceof Error ? e.message : "Unable to load skills", "error");
17
+ }));
18
+ }, [
19
+ v,
20
+ x,
21
+ y
22
+ ]);
23
+ let F = b ?? _, I = F.filter((e) => e.proficiency !== null), L = F.filter((e) => e.proficiency === null), R = async (e) => {
24
+ e.preventDefault();
25
+ let t = w.trim();
26
+ if (!t) {
27
+ y("Enter a skill name", "error");
28
+ return;
29
+ }
30
+ let n = null;
31
+ if (E.trim() !== "") {
32
+ let e = Number(E);
33
+ if (!Number.isInteger(e) || e < 0 || e > 100) {
34
+ y("Proficiency must be a whole number from 0 to 100", "error");
35
+ return;
36
+ }
37
+ n = e;
38
+ }
39
+ k(!0);
40
+ try {
41
+ await S({
42
+ employeeId: v,
43
+ skillName: t,
44
+ proficiency: n
45
+ }), T(""), D(""), y("Skill saved", "success");
46
+ } catch (e) {
47
+ y(e instanceof Error ? e.message : "Unable to add this skill", "error");
48
+ } finally {
49
+ k(!1);
50
+ }
51
+ }, z = async (e, t) => {
52
+ j(e);
53
+ try {
54
+ await C(v, e), y(`${t} removed`, "success");
55
+ } catch (e) {
56
+ y(e instanceof Error ? e.message : "Unable to remove this skill", "error");
57
+ } finally {
58
+ j(null);
59
+ }
60
+ };
61
+ return /* @__PURE__ */ l("div", {
62
+ className: "space-y-6",
63
+ "data-testid": "employee-skills-panel",
64
+ children: [b === void 0 && !M ? /* @__PURE__ */ c("div", {
65
+ className: "flex justify-center py-8",
66
+ children: /* @__PURE__ */ c(h, {})
67
+ }) : F.length === 0 ? /* @__PURE__ */ c(f, {
68
+ illustration: "empty-data",
69
+ title: "No skills recorded",
70
+ description: "Add this team member's skills below. Names are matched to the CrewFoundry skills taxonomy, so spellings like ReactJS resolve to React."
71
+ }) : /* @__PURE__ */ l("div", {
72
+ className: "space-y-5",
73
+ children: [I.length > 0 ? /* @__PURE__ */ c("ul", {
74
+ className: "space-y-4",
75
+ "aria-label": "Assessed skills",
76
+ children: I.map((e) => /* @__PURE__ */ l("li", {
77
+ className: "flex items-end gap-3",
78
+ children: [
79
+ /* @__PURE__ */ c(n, {
80
+ className: "flex-1",
81
+ label: e.skill.name,
82
+ value: e.proficiency ?? 0
83
+ }),
84
+ /* @__PURE__ */ c(u, {
85
+ variant: "secondary",
86
+ className: "shrink-0 text-xs",
87
+ children: t(e.verifiedFrom)
88
+ }),
89
+ /* @__PURE__ */ c(d, {
90
+ size: "sm",
91
+ variant: "ghost",
92
+ "aria-label": `Remove ${e.skill.name}`,
93
+ disabled: A === e.skillId,
94
+ onClick: () => void z(e.skillId, e.skill.name),
95
+ children: /* @__PURE__ */ c(s, { className: "h-3.5 w-3.5" })
96
+ })
97
+ ]
98
+ }, e.id))
99
+ }) : null, L.length > 0 ? /* @__PURE__ */ l("div", {
100
+ className: "space-y-2",
101
+ children: [/* @__PURE__ */ c("p", {
102
+ className: "text-xs font-medium uppercase tracking-wide text-text-muted",
103
+ children: "Proficiency not assessed yet"
104
+ }), /* @__PURE__ */ c("ul", {
105
+ className: "flex flex-wrap gap-2",
106
+ "aria-label": "Skills without an assessed proficiency",
107
+ children: L.map((e) => /* @__PURE__ */ l("li", {
108
+ className: "inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-surface py-1 pl-3 pr-1 text-xs text-text-primary",
109
+ children: [/* @__PURE__ */ c("span", { children: e.skill.name }), /* @__PURE__ */ c("button", {
110
+ type: "button",
111
+ className: "rounded-full p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary disabled:opacity-50",
112
+ "aria-label": `Remove ${e.skill.name}`,
113
+ disabled: A === e.skillId,
114
+ onClick: () => void z(e.skillId, e.skill.name),
115
+ children: /* @__PURE__ */ c(s, { className: "h-3 w-3" })
116
+ })]
117
+ }, e.id))
118
+ })]
119
+ }) : null]
120
+ }), /* @__PURE__ */ l("form", {
121
+ className: "flex flex-col gap-3 border-t border-border-subtle pt-4 sm:flex-row sm:items-end",
122
+ onSubmit: (e) => void R(e),
123
+ "aria-label": "Add or update a skill",
124
+ children: [
125
+ /* @__PURE__ */ l("div", {
126
+ className: "flex flex-1 flex-col gap-1.5",
127
+ children: [/* @__PURE__ */ c(m, {
128
+ htmlFor: `skill-name-${v}`,
129
+ children: "Skill"
130
+ }), /* @__PURE__ */ c(p, {
131
+ id: `skill-name-${v}`,
132
+ value: w,
133
+ disabled: O,
134
+ onChange: (e) => T(e.target.value),
135
+ placeholder: "e.g. Forklift Certification, Kubernetes, Site Safety"
136
+ })]
137
+ }),
138
+ /* @__PURE__ */ l("div", {
139
+ className: "flex flex-col gap-1.5 sm:w-36",
140
+ children: [/* @__PURE__ */ c(m, {
141
+ htmlFor: `skill-proficiency-${v}`,
142
+ children: "Proficiency %"
143
+ }), /* @__PURE__ */ c(p, {
144
+ id: `skill-proficiency-${v}`,
145
+ type: "number",
146
+ inputMode: "numeric",
147
+ min: 0,
148
+ max: 100,
149
+ step: 1,
150
+ value: E,
151
+ disabled: O,
152
+ onChange: (e) => D(e.target.value),
153
+ placeholder: "0-100"
154
+ })]
155
+ }),
156
+ /* @__PURE__ */ l(d, {
157
+ type: "submit",
158
+ disabled: O,
159
+ children: [/* @__PURE__ */ c(o, { className: "mr-1.5 h-3.5 w-3.5" }), O ? "Saving…" : "Save skill"]
160
+ })
161
+ ]
162
+ })]
163
+ });
164
+ }
165
+ //#endregion
166
+ export { v as EmployeeSkillsPanel };
167
+
168
+ //# sourceMappingURL=EmployeeSkillsPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmployeeSkillsPanel.js","names":[],"sources":["../../../src/crewfoundry/components/EmployeeSkillsPanel.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type FormEvent } from 'react';\nimport { Badge, Button, IllustratedEmptyState, Input, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Plus, X } from 'lucide-react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { SkillBar } from './viz';\n\nexport interface EmployeeSkillsPanelProps {\n employeeId: string;\n}\n\n/** Stable empty profile so the selector never hands React a fresh array. */\nconst NO_PROFILE: never[] = [];\n\n/**\n * Employee profile Skills tab (BOFF-3717, mock cf-03). Shows the employee's\n * canonical skills from the skills taxonomy with real proficiency bars.\n * Skills without an assessed proficiency (for example ones backfilled from\n * the old free-text tags) say so instead of showing an invented level.\n * Typed names resolve through taxonomy aliases, so \"ReactJS\" lands on React.\n */\nexport function EmployeeSkillsPanel({ employeeId }: EmployeeSkillsPanelProps) {\n const { showToast } = useGlobalUi();\n const profile = useCrewFoundryStore((state) => state.employeeSkillProfiles[employeeId]);\n const fetchEmployeeSkillProfile = useCrewFoundryStore((state) => state.fetchEmployeeSkillProfile);\n const attachEmployeeSkill = useCrewFoundryStore((state) => state.attachEmployeeSkill);\n const detachEmployeeSkill = useCrewFoundryStore((state) => state.detachEmployeeSkill);\n const [skillName, setSkillName] = useState('');\n const [proficiency, setProficiency] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [removingSkillId, setRemovingSkillId] = useState<string | null>(null);\n const [loadFailed, setLoadFailed] = useState(false);\n // Gates the one-time fetch per employee without triggering a re-render.\n const loadedForRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (loadedForRef.current === employeeId) return;\n loadedForRef.current = employeeId;\n fetchEmployeeSkillProfile(employeeId).catch((error: unknown) => {\n setLoadFailed(true);\n showToast(error instanceof Error ? error.message : 'Unable to load skills', 'error');\n });\n }, [employeeId, fetchEmployeeSkillProfile, showToast]);\n\n const skills = profile ?? NO_PROFILE;\n const assessed = skills.filter((item) => item.proficiency !== null);\n const unassessed = skills.filter((item) => item.proficiency === null);\n\n const handleAdd = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n const name = skillName.trim();\n if (!name) {\n showToast('Enter a skill name', 'error');\n return;\n }\n let level: number | null = null;\n if (proficiency.trim() !== '') {\n const parsed = Number(proficiency);\n if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {\n showToast('Proficiency must be a whole number from 0 to 100', 'error');\n return;\n }\n level = parsed;\n }\n setSubmitting(true);\n try {\n await attachEmployeeSkill({ employeeId, skillName: name, proficiency: level });\n setSkillName('');\n setProficiency('');\n showToast('Skill saved', 'success');\n } catch (error: unknown) {\n showToast(error instanceof Error ? error.message : 'Unable to add this skill', 'error');\n } finally {\n setSubmitting(false);\n }\n };\n\n const handleRemove = async (skillId: string, name: string) => {\n setRemovingSkillId(skillId);\n try {\n await detachEmployeeSkill(employeeId, skillId);\n showToast(`${name} removed`, 'success');\n } catch (error: unknown) {\n showToast(error instanceof Error ? error.message : 'Unable to remove this skill', 'error');\n } finally {\n setRemovingSkillId(null);\n }\n };\n\n return (\n <div className=\"space-y-6\" data-testid=\"employee-skills-panel\">\n {profile === undefined && !loadFailed ? (\n <div className=\"flex justify-center py-8\">\n <Spinner />\n </div>\n ) : skills.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No skills recorded\"\n description=\"Add this team member's skills below. Names are matched to the CrewFoundry skills taxonomy, so spellings like ReactJS resolve to React.\"\n />\n ) : (\n <div className=\"space-y-5\">\n {assessed.length > 0 ? (\n <ul className=\"space-y-4\" aria-label=\"Assessed skills\">\n {assessed.map((item) => (\n <li key={item.id} className=\"flex items-end gap-3\">\n <SkillBar\n className=\"flex-1\"\n label={item.skill.name}\n value={item.proficiency ?? 0}\n />\n <Badge variant=\"secondary\" className=\"shrink-0 text-xs\">\n {formatEnumLabel(item.verifiedFrom)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${item.skill.name}`}\n disabled={removingSkillId === item.skillId}\n onClick={() => void handleRemove(item.skillId, item.skill.name)}\n >\n <X className=\"h-3.5 w-3.5\" />\n </Button>\n </li>\n ))}\n </ul>\n ) : null}\n\n {unassessed.length > 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-muted\">\n Proficiency not assessed yet\n </p>\n <ul\n className=\"flex flex-wrap gap-2\"\n aria-label=\"Skills without an assessed proficiency\"\n >\n {unassessed.map((item) => (\n <li\n key={item.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-surface py-1 pl-3 pr-1 text-xs text-text-primary\"\n >\n <span>{item.skill.name}</span>\n <button\n type=\"button\"\n className=\"rounded-full p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary disabled:opacity-50\"\n aria-label={`Remove ${item.skill.name}`}\n disabled={removingSkillId === item.skillId}\n onClick={() => void handleRemove(item.skillId, item.skill.name)}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </li>\n ))}\n </ul>\n </div>\n ) : null}\n </div>\n )}\n\n <form\n className=\"flex flex-col gap-3 border-t border-border-subtle pt-4 sm:flex-row sm:items-end\"\n onSubmit={(event) => void handleAdd(event)}\n aria-label=\"Add or update a skill\"\n >\n <div className=\"flex flex-1 flex-col gap-1.5\">\n <Label htmlFor={`skill-name-${employeeId}`}>Skill</Label>\n <Input\n id={`skill-name-${employeeId}`}\n value={skillName}\n disabled={submitting}\n onChange={(event) => setSkillName(event.target.value)}\n placeholder=\"e.g. Forklift Certification, Kubernetes, Site Safety\"\n />\n </div>\n <div className=\"flex flex-col gap-1.5 sm:w-36\">\n <Label htmlFor={`skill-proficiency-${employeeId}`}>Proficiency %</Label>\n <Input\n id={`skill-proficiency-${employeeId}`}\n type=\"number\"\n inputMode=\"numeric\"\n min={0}\n max={100}\n step={1}\n value={proficiency}\n disabled={submitting}\n onChange={(event) => setProficiency(event.target.value)}\n placeholder=\"0-100\"\n />\n </div>\n <Button type=\"submit\" disabled={submitting}>\n <Plus className=\"mr-1.5 h-3.5 w-3.5\" />\n {submitting ? 'Saving…' : 'Save skill'}\n </Button>\n </form>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The search param that names the record a page is focused on.
3
+ *
4
+ * It is the same `?id=` the detail pages (`hiring/job-details-page?id=…`)
5
+ * already read, so `resolveCrewFoundryConversationContext` in
6
+ * `src/utils/pageContext.ts` resolves every focused record the same way: the
7
+ * assistant's "this interview" / "approve this" works without the user naming
8
+ * the record.
9
+ */
10
+ export declare const FOCUSED_RECORD_PARAM = "id";
11
+ interface UseFocusedRecordParamOptions<T extends {
12
+ id: string;
13
+ }> {
14
+ /** The records the page can focus. Restoring waits until the URL's id is among them. */
15
+ records: readonly T[];
16
+ /** The record the page is showing right now (selected row, open edit dialog), or null. */
17
+ activeId: string | null;
18
+ /** Re-focus the record named by the URL when the page is opened from a link or reloaded. */
19
+ onRestore: (record: T) => void;
20
+ }
21
+ /**
22
+ * Keeps `?id=` in step with the record a list page is focused on.
23
+ *
24
+ * List pages (leave, time, payroll, interviews, departments, clients, vendors,
25
+ * announcements, candidates) have no detail route of their own: the user
26
+ * focuses a record in place, by selecting a row or opening its edit dialog.
27
+ * Writing that focus to the URL is what lets the assistant's page context name
28
+ * the record, and makes the focused record linkable.
29
+ *
30
+ * - On first load, an `?id=` already in the URL is restored once, as soon as
31
+ * the record is loaded (`onRestore`). Until then the id is left alone, so a
32
+ * slow list load cannot erase a deep link.
33
+ * - After that, the URL follows `activeId`: set while a record is focused,
34
+ * removed when focus is cleared. Updates use `replace` so focusing rows does
35
+ * not fill the browser history.
36
+ */
37
+ export declare function useFocusedRecordParam<T extends {
38
+ id: string;
39
+ }>({ records, activeId, onRestore, }: UseFocusedRecordParamOptions<T>): void;
40
+ /**
41
+ * Marks the focused row's primary cell. `ResponsiveTable` has no row-state
42
+ * styling, so the highlight sits on the cell; the classes match the selected
43
+ * state of the interview schedule list.
44
+ */
45
+ export declare const FOCUSED_CELL_CLASS = "rounded-[var(--radius-input)] bg-action-primary-bg/10 ring-1 ring-action-primary-bg";
46
+ /** The 1-based `usePagination` page that holds `id` in `list` (1 when it is absent). */
47
+ export declare function pageContaining(list: readonly {
48
+ id: string;
49
+ }[], id: string, pageSize: number): number;
50
+ export {};
51
+ //# sourceMappingURL=useFocusedRecordParam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFocusedRecordParam.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/lib/useFocusedRecordParam.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,UAAU,4BAA4B,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC7D,wFAAwF;IACxF,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4FAA4F;IAC5F,SAAS,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAC9D,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE,4BAA4B,CAAC,CAAC,CAAC,GAAG,IAAI,CA6DxC;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,wFACwD,CAAC;AAExF,wFAAwF;AACxF,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAC/B,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,GACf,MAAM,CAGR"}
@@ -0,0 +1,38 @@
1
+ import { useEffect as e, useRef as t } from "react";
2
+ import { useSearchParams as n } from "react-router";
3
+ function r({ records: r, activeId: i, onRestore: a }) {
4
+ let [o, s] = n(), c = o.get("id"), l = t(c === null), u = t(null), d = t(a), f = t(c), p = t(s);
5
+ e(() => {
6
+ d.current = a, f.current = c, p.current = s;
7
+ }), e(() => {
8
+ if (l.current) return;
9
+ if (c === null) {
10
+ l.current = !0;
11
+ return;
12
+ }
13
+ let e = r.find((e) => e.id === c);
14
+ e && (l.current = !0, u.current = e.id, d.current(e));
15
+ }, [r, c]), e(() => {
16
+ if (!l.current) {
17
+ if (i === null) return;
18
+ l.current = !0;
19
+ }
20
+ if (u.current !== null) {
21
+ if (i !== u.current) return;
22
+ u.current = null;
23
+ }
24
+ i !== f.current && p.current((e) => {
25
+ let t = new URLSearchParams(e);
26
+ return i ? t.set("id", i) : t.delete("id"), t;
27
+ }, { replace: !0 });
28
+ }, [i]);
29
+ }
30
+ var i = "rounded-[var(--radius-input)] bg-action-primary-bg/10 ring-1 ring-action-primary-bg";
31
+ function a(e, t, n) {
32
+ let r = e.findIndex((e) => e.id === t);
33
+ return r < 0 || n <= 0 ? 1 : Math.floor(r / n) + 1;
34
+ }
35
+ //#endregion
36
+ export { i as FOCUSED_CELL_CLASS, a as pageContaining, r as useFocusedRecordParam };
37
+
38
+ //# sourceMappingURL=useFocusedRecordParam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFocusedRecordParam.js","names":[],"sources":["../../../src/crewfoundry/lib/useFocusedRecordParam.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\nimport { useSearchParams } from 'react-router';\n\n/**\n * The search param that names the record a page is focused on.\n *\n * It is the same `?id=` the detail pages (`hiring/job-details-page?id=…`)\n * already read, so `resolveCrewFoundryConversationContext` in\n * `src/utils/pageContext.ts` resolves every focused record the same way: the\n * assistant's \"this interview\" / \"approve this\" works without the user naming\n * the record.\n */\nexport const FOCUSED_RECORD_PARAM = 'id';\n\ninterface UseFocusedRecordParamOptions<T extends { id: string }> {\n /** The records the page can focus. Restoring waits until the URL's id is among them. */\n records: readonly T[];\n /** The record the page is showing right now (selected row, open edit dialog), or null. */\n activeId: string | null;\n /** Re-focus the record named by the URL when the page is opened from a link or reloaded. */\n onRestore: (record: T) => void;\n}\n\n/**\n * Keeps `?id=` in step with the record a list page is focused on.\n *\n * List pages (leave, time, payroll, interviews, departments, clients, vendors,\n * announcements, candidates) have no detail route of their own: the user\n * focuses a record in place, by selecting a row or opening its edit dialog.\n * Writing that focus to the URL is what lets the assistant's page context name\n * the record, and makes the focused record linkable.\n *\n * - On first load, an `?id=` already in the URL is restored once, as soon as\n * the record is loaded (`onRestore`). Until then the id is left alone, so a\n * slow list load cannot erase a deep link.\n * - After that, the URL follows `activeId`: set while a record is focused,\n * removed when focus is cleared. Updates use `replace` so focusing rows does\n * not fill the browser history.\n */\nexport function useFocusedRecordParam<T extends { id: string }>({\n records,\n activeId,\n onRestore,\n}: UseFocusedRecordParamOptions<T>): void {\n const [searchParams, setSearchParams] = useSearchParams();\n const urlId = searchParams.get(FOCUSED_RECORD_PARAM);\n\n // The initial URL id is restored at most once; afterwards the page owns focus.\n const restoredRef = useRef<boolean>(urlId === null);\n // Set while a restored record has been handed to the page but the page's\n // focus has not caught up yet, so the sync below does not erase the id in\n // the render between the two.\n const awaitingFocusRef = useRef<string | null>(null);\n const onRestoreRef = useRef(onRestore);\n // Read the latest URL state inside the sync effect without re-running it on\n // every navigation: it must fire only when the page's focus changes.\n const urlIdRef = useRef(urlId);\n const setSearchParamsRef = useRef(setSearchParams);\n\n // Runs before the effects below in every commit, so they see this render's values.\n useEffect(() => {\n onRestoreRef.current = onRestore;\n urlIdRef.current = urlId;\n setSearchParamsRef.current = setSearchParams;\n });\n\n useEffect(() => {\n if (restoredRef.current) return;\n if (urlId === null) {\n restoredRef.current = true;\n return;\n }\n const record = records.find((candidate) => candidate.id === urlId);\n if (!record) return;\n restoredRef.current = true;\n awaitingFocusRef.current = record.id;\n onRestoreRef.current(record);\n }, [records, urlId]);\n\n useEffect(() => {\n if (!restoredRef.current) {\n // The linked record never loaded (deleted, or not visible to this\n // user). Once the user focuses something else, the page owns the URL.\n if (activeId === null) return;\n restoredRef.current = true;\n }\n if (awaitingFocusRef.current !== null) {\n if (activeId !== awaitingFocusRef.current) return;\n awaitingFocusRef.current = null;\n }\n if (activeId === urlIdRef.current) return;\n setSearchParamsRef.current(\n (previous) => {\n const next = new URLSearchParams(previous);\n if (activeId) {\n next.set(FOCUSED_RECORD_PARAM, activeId);\n } else {\n next.delete(FOCUSED_RECORD_PARAM);\n }\n return next;\n },\n { replace: true }\n );\n }, [activeId]);\n}\n\n/**\n * Marks the focused row's primary cell. `ResponsiveTable` has no row-state\n * styling, so the highlight sits on the cell; the classes match the selected\n * state of the interview schedule list.\n */\nexport const FOCUSED_CELL_CLASS =\n 'rounded-[var(--radius-input)] bg-action-primary-bg/10 ring-1 ring-action-primary-bg';\n\n/** The 1-based `usePagination` page that holds `id` in `list` (1 when it is absent). */\nexport function pageContaining(\n list: readonly { id: string }[],\n id: string,\n pageSize: number\n): number {\n const index = list.findIndex((item) => item.id === id);\n return index < 0 || pageSize <= 0 ? 1 : Math.floor(index / pageSize) + 1;\n}\n"],"mappings":";;AAuCA,SAAgB,EAAgD,EAC9D,YACA,aACA,gBACwC;CACxC,IAAM,CAAC,GAAc,KAAmB,GAAiB,EACnD,IAAQ,EAAa,IAAA,KAAyB,EAG9C,IAAc,EAAgB,MAAU,KAAK,EAI7C,IAAmB,EAAsB,KAAK,EAC9C,IAAe,EAAO,EAAU,EAGhC,IAAW,EAAO,EAAM,EACxB,IAAqB,EAAO,EAAgB;AAsBlD,CAnBA,QAAgB;AAGd,EAFA,EAAa,UAAU,GACvB,EAAS,UAAU,GACnB,EAAmB,UAAU;GAC7B,EAEF,QAAgB;AACd,MAAI,EAAY,QAAS;AACzB,MAAI,MAAU,MAAM;AAClB,KAAY,UAAU;AACtB;;EAEF,IAAM,IAAS,EAAQ,MAAM,MAAc,EAAU,OAAO,EAAM;AAC7D,QACL,EAAY,UAAU,IACtB,EAAiB,UAAU,EAAO,IAClC,EAAa,QAAQ,EAAO;IAC3B,CAAC,GAAS,EAAM,CAAC,EAEpB,QAAgB;AACd,MAAI,CAAC,EAAY,SAAS;AAGxB,OAAI,MAAa,KAAM;AACvB,KAAY,UAAU;;AAExB,MAAI,EAAiB,YAAY,MAAM;AACrC,OAAI,MAAa,EAAiB,QAAS;AAC3C,KAAiB,UAAU;;AAEzB,QAAa,EAAS,WAC1B,EAAmB,SAChB,MAAa;GACZ,IAAM,IAAO,IAAI,gBAAgB,EAAS;AAM1C,UALI,IACF,EAAK,IAAA,MAA0B,EAAS,GAExC,EAAK,OAAA,KAA4B,EAE5B;KAET,EAAE,SAAS,IAAM,CAClB;IACA,CAAC,EAAS,CAAC;;AAQhB,IAAa,IACX;AAGF,SAAgB,EACd,GACA,GACA,GACQ;CACR,IAAM,IAAQ,EAAK,WAAW,MAAS,EAAK,OAAO,EAAG;AACtD,QAAO,IAAQ,KAAK,KAAY,IAAI,IAAI,KAAK,MAAM,IAAQ,EAAS,GAAG"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommunicationsAnnouncementsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/CommunicationsAnnouncementsPagePage.tsx"],"names":[],"mappings":"AAiHA,wBAAgB,mCAAmC,gCAiVlD"}
1
+ {"version":3,"file":"CommunicationsAnnouncementsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/CommunicationsAnnouncementsPagePage.tsx"],"names":[],"mappings":"AAkHA,wBAAgB,mCAAmC,gCAuVlD"}