@company-semantics/contracts 13.10.0 → 13.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/generated.ts +217 -0
- package/src/generated/openapi-routes.ts +3 -0
- package/src/identity/index.ts +2 -0
- package/src/identity/people-org-chart.ts +19 -0
- package/src/index.ts +2 -0
- package/src/org/__tests__/org-units.test.ts +105 -0
- package/src/org/index.ts +16 -0
- package/src/org/org-units.ts +18 -0
- package/src/org/schemas.ts +109 -0
- package/src/org/types.ts +11 -0
package/package.json
CHANGED
package/src/api/generated.ts
CHANGED
|
@@ -2041,6 +2041,58 @@ export interface paths {
|
|
|
2041
2041
|
patch?: never;
|
|
2042
2042
|
trace?: never;
|
|
2043
2043
|
};
|
|
2044
|
+
"/api/org-units/{unitId}/open-roles": {
|
|
2045
|
+
parameters: {
|
|
2046
|
+
query?: never;
|
|
2047
|
+
header?: never;
|
|
2048
|
+
path?: never;
|
|
2049
|
+
cookie?: never;
|
|
2050
|
+
};
|
|
2051
|
+
/** List open roles of an org unit */
|
|
2052
|
+
get: operations["listOrgUnitOpenRoles"];
|
|
2053
|
+
put?: never;
|
|
2054
|
+
/** Create an open role (placeholder seat) in an org unit */
|
|
2055
|
+
post: operations["createOrgUnitOpenRole"];
|
|
2056
|
+
delete?: never;
|
|
2057
|
+
options?: never;
|
|
2058
|
+
head?: never;
|
|
2059
|
+
patch?: never;
|
|
2060
|
+
trace?: never;
|
|
2061
|
+
};
|
|
2062
|
+
"/api/org-units/{unitId}/open-roles/{roleId}": {
|
|
2063
|
+
parameters: {
|
|
2064
|
+
query?: never;
|
|
2065
|
+
header?: never;
|
|
2066
|
+
path?: never;
|
|
2067
|
+
cookie?: never;
|
|
2068
|
+
};
|
|
2069
|
+
get?: never;
|
|
2070
|
+
put?: never;
|
|
2071
|
+
post?: never;
|
|
2072
|
+
delete?: never;
|
|
2073
|
+
options?: never;
|
|
2074
|
+
head?: never;
|
|
2075
|
+
/** Advance (hiring) or close an open role */
|
|
2076
|
+
patch: operations["updateOrgUnitOpenRoleStatus"];
|
|
2077
|
+
trace?: never;
|
|
2078
|
+
};
|
|
2079
|
+
"/api/org-units/{unitId}/open-roles/{roleId}/fill": {
|
|
2080
|
+
parameters: {
|
|
2081
|
+
query?: never;
|
|
2082
|
+
header?: never;
|
|
2083
|
+
path?: never;
|
|
2084
|
+
cookie?: never;
|
|
2085
|
+
};
|
|
2086
|
+
get?: never;
|
|
2087
|
+
put?: never;
|
|
2088
|
+
/** Fill an open role by seating an existing member or inviting a person */
|
|
2089
|
+
post: operations["fillOrgUnitOpenRole"];
|
|
2090
|
+
delete?: never;
|
|
2091
|
+
options?: never;
|
|
2092
|
+
head?: never;
|
|
2093
|
+
patch?: never;
|
|
2094
|
+
trace?: never;
|
|
2095
|
+
};
|
|
2044
2096
|
"/api/users/org-chart": {
|
|
2045
2097
|
parameters: {
|
|
2046
2098
|
query?: never;
|
|
@@ -4221,6 +4273,7 @@ export interface components {
|
|
|
4221
4273
|
depth: number;
|
|
4222
4274
|
hasChildren: boolean;
|
|
4223
4275
|
memberCount: number;
|
|
4276
|
+
openRoleCount: number;
|
|
4224
4277
|
missingAtNextLevel: {
|
|
4225
4278
|
count: number;
|
|
4226
4279
|
userIds: string[];
|
|
@@ -4368,6 +4421,7 @@ export interface components {
|
|
|
4368
4421
|
depth: number;
|
|
4369
4422
|
hasChildren: boolean;
|
|
4370
4423
|
memberCount: number;
|
|
4424
|
+
openRoleCount: number;
|
|
4371
4425
|
missingAtNextLevel: {
|
|
4372
4426
|
count: number;
|
|
4373
4427
|
userIds: string[];
|
|
@@ -4570,6 +4624,42 @@ export interface components {
|
|
|
4570
4624
|
updatedAt: string;
|
|
4571
4625
|
};
|
|
4572
4626
|
};
|
|
4627
|
+
OpenRoleListResponse: {
|
|
4628
|
+
/** Format: uuid */
|
|
4629
|
+
unitId: string;
|
|
4630
|
+
openRoles: {
|
|
4631
|
+
/** Format: uuid */
|
|
4632
|
+
id: string;
|
|
4633
|
+
/** Format: uuid */
|
|
4634
|
+
orgId: string;
|
|
4635
|
+
/** Format: uuid */
|
|
4636
|
+
unitId: string;
|
|
4637
|
+
title: string | null;
|
|
4638
|
+
targetStartDate: string | null;
|
|
4639
|
+
/** @enum {string} */
|
|
4640
|
+
status: "open" | "hiring" | "filled" | "closed";
|
|
4641
|
+
filledByUserId: string | null;
|
|
4642
|
+
createdAt: string;
|
|
4643
|
+
updatedAt: string;
|
|
4644
|
+
}[];
|
|
4645
|
+
};
|
|
4646
|
+
OpenRoleResponse: {
|
|
4647
|
+
openRole: {
|
|
4648
|
+
/** Format: uuid */
|
|
4649
|
+
id: string;
|
|
4650
|
+
/** Format: uuid */
|
|
4651
|
+
orgId: string;
|
|
4652
|
+
/** Format: uuid */
|
|
4653
|
+
unitId: string;
|
|
4654
|
+
title: string | null;
|
|
4655
|
+
targetStartDate: string | null;
|
|
4656
|
+
/** @enum {string} */
|
|
4657
|
+
status: "open" | "hiring" | "filled" | "closed";
|
|
4658
|
+
filledByUserId: string | null;
|
|
4659
|
+
createdAt: string;
|
|
4660
|
+
updatedAt: string;
|
|
4661
|
+
};
|
|
4662
|
+
};
|
|
4573
4663
|
PeopleOrgChartResponse: {
|
|
4574
4664
|
nodes: {
|
|
4575
4665
|
/** Format: uuid */
|
|
@@ -4587,6 +4677,13 @@ export interface components {
|
|
|
4587
4677
|
/** @enum {string} */
|
|
4588
4678
|
relationshipType: "solid" | "dotted";
|
|
4589
4679
|
}[];
|
|
4680
|
+
openRoles: {
|
|
4681
|
+
/** Format: uuid */
|
|
4682
|
+
id: string;
|
|
4683
|
+
/** Format: uuid */
|
|
4684
|
+
unitId: string;
|
|
4685
|
+
title: string | null;
|
|
4686
|
+
}[];
|
|
4590
4687
|
};
|
|
4591
4688
|
/** @description Polling snapshot of a generic ingestion operation. */
|
|
4592
4689
|
IngestionOperationPollResponse: {
|
|
@@ -8463,6 +8560,126 @@ export interface operations {
|
|
|
8463
8560
|
};
|
|
8464
8561
|
};
|
|
8465
8562
|
};
|
|
8563
|
+
listOrgUnitOpenRoles: {
|
|
8564
|
+
parameters: {
|
|
8565
|
+
query?: never;
|
|
8566
|
+
header?: never;
|
|
8567
|
+
path: {
|
|
8568
|
+
unitId: string;
|
|
8569
|
+
};
|
|
8570
|
+
cookie?: never;
|
|
8571
|
+
};
|
|
8572
|
+
requestBody?: never;
|
|
8573
|
+
responses: {
|
|
8574
|
+
/** @description Open roles of the unit (all lifecycle states) */
|
|
8575
|
+
200: {
|
|
8576
|
+
headers: {
|
|
8577
|
+
[name: string]: unknown;
|
|
8578
|
+
};
|
|
8579
|
+
content: {
|
|
8580
|
+
"application/json": components["schemas"]["OpenRoleListResponse"];
|
|
8581
|
+
};
|
|
8582
|
+
};
|
|
8583
|
+
};
|
|
8584
|
+
};
|
|
8585
|
+
createOrgUnitOpenRole: {
|
|
8586
|
+
parameters: {
|
|
8587
|
+
query?: never;
|
|
8588
|
+
header?: never;
|
|
8589
|
+
path: {
|
|
8590
|
+
unitId: string;
|
|
8591
|
+
};
|
|
8592
|
+
cookie?: never;
|
|
8593
|
+
};
|
|
8594
|
+
requestBody: {
|
|
8595
|
+
content: {
|
|
8596
|
+
"application/json": {
|
|
8597
|
+
title?: string | null;
|
|
8598
|
+
targetStartDate?: string | null;
|
|
8599
|
+
};
|
|
8600
|
+
};
|
|
8601
|
+
};
|
|
8602
|
+
responses: {
|
|
8603
|
+
/** @description Created open role */
|
|
8604
|
+
201: {
|
|
8605
|
+
headers: {
|
|
8606
|
+
[name: string]: unknown;
|
|
8607
|
+
};
|
|
8608
|
+
content: {
|
|
8609
|
+
"application/json": components["schemas"]["OpenRoleResponse"];
|
|
8610
|
+
};
|
|
8611
|
+
};
|
|
8612
|
+
};
|
|
8613
|
+
};
|
|
8614
|
+
updateOrgUnitOpenRoleStatus: {
|
|
8615
|
+
parameters: {
|
|
8616
|
+
query?: never;
|
|
8617
|
+
header?: never;
|
|
8618
|
+
path: {
|
|
8619
|
+
unitId: string;
|
|
8620
|
+
roleId: string;
|
|
8621
|
+
};
|
|
8622
|
+
cookie?: never;
|
|
8623
|
+
};
|
|
8624
|
+
requestBody: {
|
|
8625
|
+
content: {
|
|
8626
|
+
"application/json": {
|
|
8627
|
+
/** @enum {string} */
|
|
8628
|
+
status: "hiring" | "closed";
|
|
8629
|
+
};
|
|
8630
|
+
};
|
|
8631
|
+
};
|
|
8632
|
+
responses: {
|
|
8633
|
+
/** @description Updated open role */
|
|
8634
|
+
200: {
|
|
8635
|
+
headers: {
|
|
8636
|
+
[name: string]: unknown;
|
|
8637
|
+
};
|
|
8638
|
+
content: {
|
|
8639
|
+
"application/json": components["schemas"]["OpenRoleResponse"];
|
|
8640
|
+
};
|
|
8641
|
+
};
|
|
8642
|
+
};
|
|
8643
|
+
};
|
|
8644
|
+
fillOrgUnitOpenRole: {
|
|
8645
|
+
parameters: {
|
|
8646
|
+
query?: never;
|
|
8647
|
+
header?: never;
|
|
8648
|
+
path: {
|
|
8649
|
+
unitId: string;
|
|
8650
|
+
roleId: string;
|
|
8651
|
+
};
|
|
8652
|
+
cookie?: never;
|
|
8653
|
+
};
|
|
8654
|
+
requestBody: {
|
|
8655
|
+
content: {
|
|
8656
|
+
"application/json": {
|
|
8657
|
+
/** @constant */
|
|
8658
|
+
mode: "existing";
|
|
8659
|
+
/** Format: uuid */
|
|
8660
|
+
userId: string;
|
|
8661
|
+
} | {
|
|
8662
|
+
/** @constant */
|
|
8663
|
+
mode: "invite";
|
|
8664
|
+
/** Format: email */
|
|
8665
|
+
email: string;
|
|
8666
|
+
/** @enum {string} */
|
|
8667
|
+
role: "admin" | "member";
|
|
8668
|
+
};
|
|
8669
|
+
};
|
|
8670
|
+
};
|
|
8671
|
+
responses: {
|
|
8672
|
+
/** @description Filled open role */
|
|
8673
|
+
200: {
|
|
8674
|
+
headers: {
|
|
8675
|
+
[name: string]: unknown;
|
|
8676
|
+
};
|
|
8677
|
+
content: {
|
|
8678
|
+
"application/json": components["schemas"]["OpenRoleResponse"];
|
|
8679
|
+
};
|
|
8680
|
+
};
|
|
8681
|
+
};
|
|
8682
|
+
};
|
|
8466
8683
|
getPeopleOrgChart: {
|
|
8467
8684
|
parameters: {
|
|
8468
8685
|
query?: never;
|
|
@@ -86,6 +86,9 @@ export const openApiRoutes = {
|
|
|
86
86
|
'/api/org-units/{unitId}/memberships/{userId}': ['DELETE'],
|
|
87
87
|
'/api/org-units/{unitId}/memberships/{userId}/role': ['PUT'],
|
|
88
88
|
'/api/org-units/{unitId}/my-authority': ['GET'],
|
|
89
|
+
'/api/org-units/{unitId}/open-roles': ['GET', 'POST'],
|
|
90
|
+
'/api/org-units/{unitId}/open-roles/{roleId}': ['PATCH'],
|
|
91
|
+
'/api/org-units/{unitId}/open-roles/{roleId}/fill': ['POST'],
|
|
89
92
|
'/api/org-units/{unitId}/owners': ['GET'],
|
|
90
93
|
'/api/org-units/{unitId}/permissions': ['GET'],
|
|
91
94
|
'/api/org-units/{unitId}/relationships': ['GET', 'POST'],
|
package/src/identity/index.ts
CHANGED
|
@@ -66,11 +66,13 @@ export {
|
|
|
66
66
|
ReportingRelationshipTypeSchema,
|
|
67
67
|
PeopleOrgChartNodeSchema,
|
|
68
68
|
PeopleOrgChartEdgeSchema,
|
|
69
|
+
PeopleOrgChartOpenRoleSchema,
|
|
69
70
|
PeopleOrgChartResponseSchema,
|
|
70
71
|
} from "./people-org-chart";
|
|
71
72
|
export type {
|
|
72
73
|
ReportingRelationshipType,
|
|
73
74
|
PeopleOrgChartNode,
|
|
74
75
|
PeopleOrgChartEdge,
|
|
76
|
+
PeopleOrgChartOpenRole,
|
|
75
77
|
PeopleOrgChartResponse,
|
|
76
78
|
} from "./people-org-chart";
|
|
@@ -48,6 +48,24 @@ export const PeopleOrgChartEdgeSchema = z.object({
|
|
|
48
48
|
|
|
49
49
|
export type PeopleOrgChartEdge = z.infer<typeof PeopleOrgChartEdgeSchema>;
|
|
50
50
|
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Open role — a persisted placeholder seat rendered as a dashed "Open role"
|
|
53
|
+
// card in its unit's sibling column. Only active roles (open + hiring) are
|
|
54
|
+
// included; filled/closed roles never appear here. `unitId` seats the card in
|
|
55
|
+
// the right column and resolves its attach-person, mirroring how empty-unit
|
|
56
|
+
// placeholders and pending-invite ghosts are positioned.
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export const PeopleOrgChartOpenRoleSchema = z.object({
|
|
60
|
+
id: z.string().uuid(),
|
|
61
|
+
unitId: z.string().uuid(),
|
|
62
|
+
title: z.string().nullable(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export type PeopleOrgChartOpenRole = z.infer<
|
|
66
|
+
typeof PeopleOrgChartOpenRoleSchema
|
|
67
|
+
>;
|
|
68
|
+
|
|
51
69
|
// ---------------------------------------------------------------------------
|
|
52
70
|
// GET /api/users/org-chart
|
|
53
71
|
// ---------------------------------------------------------------------------
|
|
@@ -55,6 +73,7 @@ export type PeopleOrgChartEdge = z.infer<typeof PeopleOrgChartEdgeSchema>;
|
|
|
55
73
|
export const PeopleOrgChartResponseSchema = z.object({
|
|
56
74
|
nodes: z.array(PeopleOrgChartNodeSchema),
|
|
57
75
|
edges: z.array(PeopleOrgChartEdgeSchema),
|
|
76
|
+
openRoles: z.array(PeopleOrgChartOpenRoleSchema),
|
|
58
77
|
});
|
|
59
78
|
|
|
60
79
|
export type PeopleOrgChartResponse = z.infer<
|
package/src/index.ts
CHANGED
|
@@ -172,12 +172,14 @@ export {
|
|
|
172
172
|
ReportingRelationshipTypeSchema,
|
|
173
173
|
PeopleOrgChartNodeSchema,
|
|
174
174
|
PeopleOrgChartEdgeSchema,
|
|
175
|
+
PeopleOrgChartOpenRoleSchema,
|
|
175
176
|
PeopleOrgChartResponseSchema,
|
|
176
177
|
} from "./identity/index";
|
|
177
178
|
export type {
|
|
178
179
|
ReportingRelationshipType,
|
|
179
180
|
PeopleOrgChartNode,
|
|
180
181
|
PeopleOrgChartEdge,
|
|
182
|
+
PeopleOrgChartOpenRole,
|
|
181
183
|
PeopleOrgChartResponse,
|
|
182
184
|
} from "./identity/index";
|
|
183
185
|
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
OrgUnitMembershipSourceSchema,
|
|
15
15
|
OrgUnitPermissionsEntrySchema,
|
|
16
16
|
ListOrgUnitPermissionsResponseSchema,
|
|
17
|
+
OpenRoleSchema,
|
|
18
|
+
CreateOpenRoleRequestSchema,
|
|
19
|
+
UpdateOpenRoleStatusRequestSchema,
|
|
20
|
+
FillOpenRoleRequestSchema,
|
|
17
21
|
} from "../schemas.js";
|
|
18
22
|
|
|
19
23
|
const UUID_A = "11111111-1111-4111-8111-111111111111";
|
|
@@ -92,12 +96,113 @@ describe("OrgUnitTreeNodeSchema", () => {
|
|
|
92
96
|
depth: 3,
|
|
93
97
|
hasChildren: true,
|
|
94
98
|
memberCount: 5,
|
|
99
|
+
openRoleCount: 1,
|
|
95
100
|
missingAtNextLevel: null,
|
|
96
101
|
};
|
|
97
102
|
expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
|
|
98
103
|
expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 0 })).toThrow();
|
|
99
104
|
expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 6 })).toThrow();
|
|
100
105
|
});
|
|
106
|
+
|
|
107
|
+
it("requires a non-negative openRoleCount", () => {
|
|
108
|
+
const base = {
|
|
109
|
+
...makeUnit(),
|
|
110
|
+
depth: 3,
|
|
111
|
+
hasChildren: true,
|
|
112
|
+
memberCount: 5,
|
|
113
|
+
openRoleCount: 0,
|
|
114
|
+
missingAtNextLevel: null,
|
|
115
|
+
};
|
|
116
|
+
expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
|
|
117
|
+
expect(() =>
|
|
118
|
+
OrgUnitTreeNodeSchema.parse({ ...base, openRoleCount: -1 }),
|
|
119
|
+
).toThrow();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("OpenRoleSchema", () => {
|
|
124
|
+
const makeOpenRole = (overrides: Record<string, unknown> = {}) => ({
|
|
125
|
+
id: UUID_A,
|
|
126
|
+
orgId: UUID_B,
|
|
127
|
+
unitId: UUID_C,
|
|
128
|
+
title: "Senior Engineer",
|
|
129
|
+
targetStartDate: "2026-09-01",
|
|
130
|
+
status: "open",
|
|
131
|
+
filledByUserId: null,
|
|
132
|
+
createdAt: "2026-04-17T00:00:00Z",
|
|
133
|
+
updatedAt: "2026-04-17T00:00:00Z",
|
|
134
|
+
...overrides,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("accepts a valid open role and the four lifecycle states", () => {
|
|
138
|
+
expect(() => OpenRoleSchema.parse(makeOpenRole())).not.toThrow();
|
|
139
|
+
for (const status of ["open", "hiring", "filled", "closed"]) {
|
|
140
|
+
expect(() =>
|
|
141
|
+
OpenRoleSchema.parse(makeOpenRole({ status })),
|
|
142
|
+
).not.toThrow();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("allows null title and targetStartDate, rejects unknown status", () => {
|
|
147
|
+
expect(() =>
|
|
148
|
+
OpenRoleSchema.parse(
|
|
149
|
+
makeOpenRole({ title: null, targetStartDate: null }),
|
|
150
|
+
),
|
|
151
|
+
).not.toThrow();
|
|
152
|
+
expect(() =>
|
|
153
|
+
OpenRoleSchema.parse(makeOpenRole({ status: "vacant" })),
|
|
154
|
+
).toThrow();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("Open role request schemas", () => {
|
|
159
|
+
it("CreateOpenRoleRequest accepts empty body and ISO date, rejects bad date", () => {
|
|
160
|
+
expect(() => CreateOpenRoleRequestSchema.parse({})).not.toThrow();
|
|
161
|
+
expect(() =>
|
|
162
|
+
CreateOpenRoleRequestSchema.parse({
|
|
163
|
+
title: "PM",
|
|
164
|
+
targetStartDate: "2026-09-01",
|
|
165
|
+
}),
|
|
166
|
+
).not.toThrow();
|
|
167
|
+
expect(() =>
|
|
168
|
+
CreateOpenRoleRequestSchema.parse({ targetStartDate: "Sept 1" }),
|
|
169
|
+
).toThrow();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("UpdateOpenRoleStatusRequest only accepts hiring|closed", () => {
|
|
173
|
+
expect(() =>
|
|
174
|
+
UpdateOpenRoleStatusRequestSchema.parse({ status: "hiring" }),
|
|
175
|
+
).not.toThrow();
|
|
176
|
+
expect(() =>
|
|
177
|
+
UpdateOpenRoleStatusRequestSchema.parse({ status: "closed" }),
|
|
178
|
+
).not.toThrow();
|
|
179
|
+
expect(() =>
|
|
180
|
+
UpdateOpenRoleStatusRequestSchema.parse({ status: "filled" }),
|
|
181
|
+
).toThrow();
|
|
182
|
+
expect(() =>
|
|
183
|
+
UpdateOpenRoleStatusRequestSchema.parse({ status: "open" }),
|
|
184
|
+
).toThrow();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("FillOpenRoleRequest discriminates on mode", () => {
|
|
188
|
+
expect(() =>
|
|
189
|
+
FillOpenRoleRequestSchema.parse({ mode: "existing", userId: UUID_A }),
|
|
190
|
+
).not.toThrow();
|
|
191
|
+
expect(() =>
|
|
192
|
+
FillOpenRoleRequestSchema.parse({
|
|
193
|
+
mode: "invite",
|
|
194
|
+
email: "a@b.com",
|
|
195
|
+
role: "member",
|
|
196
|
+
}),
|
|
197
|
+
).not.toThrow();
|
|
198
|
+
// wrong branch fields
|
|
199
|
+
expect(() =>
|
|
200
|
+
FillOpenRoleRequestSchema.parse({ mode: "existing", email: "a@b.com" }),
|
|
201
|
+
).toThrow();
|
|
202
|
+
expect(() =>
|
|
203
|
+
FillOpenRoleRequestSchema.parse({ mode: "invite", userId: UUID_A }),
|
|
204
|
+
).toThrow();
|
|
205
|
+
});
|
|
101
206
|
});
|
|
102
207
|
|
|
103
208
|
describe("OrgUnitMembershipSchema", () => {
|
package/src/org/index.ts
CHANGED
|
@@ -219,6 +219,7 @@ export type {
|
|
|
219
219
|
OrgUnitRelationshipRole,
|
|
220
220
|
OrgUnitMembershipStatus,
|
|
221
221
|
OrgUnitMembershipSource,
|
|
222
|
+
OpenRoleStatus,
|
|
222
223
|
OrgUnitErrorCode,
|
|
223
224
|
OrgTreeResponse,
|
|
224
225
|
} from "./org-units";
|
|
@@ -236,9 +237,17 @@ export {
|
|
|
236
237
|
OrgUnitSchema,
|
|
237
238
|
OrgUnitTreeNodeSchema,
|
|
238
239
|
OrgUnitMembershipSchema,
|
|
240
|
+
OpenRoleStatusSchema,
|
|
241
|
+
OpenRoleSchema,
|
|
242
|
+
OpenRoleResponseSchema,
|
|
243
|
+
OpenRoleListResponseSchema,
|
|
244
|
+
CreateOpenRoleRequestSchema,
|
|
245
|
+
UpdateOpenRoleStatusRequestSchema,
|
|
246
|
+
FillOpenRoleRequestSchema,
|
|
239
247
|
OrgUnitRelationshipSchema,
|
|
240
248
|
OrgLevelConfigSchema,
|
|
241
249
|
OrgLevelIconSchema,
|
|
250
|
+
OrgOwnerIconSchema,
|
|
242
251
|
OrgUnitResponseSchema,
|
|
243
252
|
OrgUnitTreeResponseSchema,
|
|
244
253
|
MissingAtNextLevelSchema,
|
|
@@ -259,9 +268,16 @@ export type {
|
|
|
259
268
|
OrgUnit,
|
|
260
269
|
OrgUnitTreeNode,
|
|
261
270
|
OrgUnitMembership,
|
|
271
|
+
OpenRole,
|
|
272
|
+
OpenRoleResponse,
|
|
273
|
+
OpenRoleListResponse,
|
|
274
|
+
CreateOpenRoleRequest,
|
|
275
|
+
UpdateOpenRoleStatusRequest,
|
|
276
|
+
FillOpenRoleRequest,
|
|
262
277
|
OrgUnitRelationship,
|
|
263
278
|
OrgLevelConfig,
|
|
264
279
|
OrgLevelIcon,
|
|
280
|
+
OrgOwnerIcon,
|
|
265
281
|
OrgUnitResponse,
|
|
266
282
|
OrgUnitTreeResponse,
|
|
267
283
|
MissingAtNextLevel,
|
package/src/org/org-units.ts
CHANGED
|
@@ -56,6 +56,19 @@ export type OrgUnitMembershipSource =
|
|
|
56
56
|
| "scim"
|
|
57
57
|
| "hris";
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Hiring lifecycle of an open role — a persisted placeholder for the person
|
|
61
|
+
* who will sit in an org unit (PRD open-roles). A unit is seeded with one
|
|
62
|
+
* `open` role at creation so it never renders as a unit with nobody.
|
|
63
|
+
*
|
|
64
|
+
* Transitions: `open → hiring → filled` and `open|hiring → closed`. `filled`
|
|
65
|
+
* and `closed` are terminal. `filled` means a real `org_unit_membership` was
|
|
66
|
+
* created from this role (the row is kept as staffing history); `closed` means
|
|
67
|
+
* the seat was cancelled. Only `open` and `hiring` are "active" — they are the
|
|
68
|
+
* states that render an Open role card and count toward `openRoleCount`.
|
|
69
|
+
*/
|
|
70
|
+
export type OpenRoleStatus = "open" | "hiring" | "filled" | "closed";
|
|
71
|
+
|
|
59
72
|
/**
|
|
60
73
|
* Canonical route path constants for the `/api/org-units` surface.
|
|
61
74
|
*
|
|
@@ -87,6 +100,11 @@ export const ORG_UNITS_ROUTES = {
|
|
|
87
100
|
membershipRole: (unitId: string, userId: string) =>
|
|
88
101
|
`/api/org-units/${unitId}/memberships/${userId}/role`,
|
|
89
102
|
permissions: (unitId: string) => `/api/org-units/${unitId}/permissions`,
|
|
103
|
+
openRoles: (unitId: string) => `/api/org-units/${unitId}/open-roles`,
|
|
104
|
+
openRoleById: (unitId: string, roleId: string) =>
|
|
105
|
+
`/api/org-units/${unitId}/open-roles/${roleId}`,
|
|
106
|
+
fillOpenRole: (unitId: string, roleId: string) =>
|
|
107
|
+
`/api/org-units/${unitId}/open-roles/${roleId}/fill`,
|
|
90
108
|
} as const;
|
|
91
109
|
|
|
92
110
|
/**
|
package/src/org/schemas.ts
CHANGED
|
@@ -158,6 +158,19 @@ export type WorkspaceAccessResponse = z.infer<
|
|
|
158
158
|
// PATCH /api/workspace/name (returns WorkspaceOverview)
|
|
159
159
|
// ---------------------------------------------------------------------------
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Icon options for the `owner` org-chart role (ADR-CONT-070). Selected
|
|
163
|
+
* alongside the owner title in the Organization structure editor; `crown` is
|
|
164
|
+
* the default. Presentation only — never an authorization input.
|
|
165
|
+
*/
|
|
166
|
+
export const OrgOwnerIconSchema = z.enum([
|
|
167
|
+
"crown",
|
|
168
|
+
"crown-cross",
|
|
169
|
+
"person",
|
|
170
|
+
"person-simple-circle",
|
|
171
|
+
"medal-military",
|
|
172
|
+
]);
|
|
173
|
+
|
|
161
174
|
export const WorkspaceOverviewSchema = z.object({
|
|
162
175
|
id: z.string(),
|
|
163
176
|
name: z.string(),
|
|
@@ -173,11 +186,19 @@ export const WorkspaceOverviewSchema = z.object({
|
|
|
173
186
|
* section heading renders. Presentation only — never an authorization input.
|
|
174
187
|
*/
|
|
175
188
|
ownerTitle: z.string(),
|
|
189
|
+
/**
|
|
190
|
+
* Org-configured icon for the `owner` org-chart role (ADR-CONT-070).
|
|
191
|
+
* Server-resolved: `orgs.owner_icon` when set, otherwise the default
|
|
192
|
+
* "crown". Always present. Presentation only — never an authorization input.
|
|
193
|
+
*/
|
|
194
|
+
ownerIcon: OrgOwnerIconSchema,
|
|
176
195
|
createdAt: z.string(),
|
|
177
196
|
memberCount: z.number(),
|
|
178
197
|
claimable: z.boolean(),
|
|
179
198
|
});
|
|
180
199
|
|
|
200
|
+
export type OrgOwnerIcon = z.infer<typeof OrgOwnerIconSchema>;
|
|
201
|
+
|
|
181
202
|
export type WorkspaceOverview = z.infer<typeof WorkspaceOverviewSchema>;
|
|
182
203
|
|
|
183
204
|
// ---------------------------------------------------------------------------
|
|
@@ -855,6 +876,18 @@ export const OrgUnitMembershipSourceSchema = z.enum([
|
|
|
855
876
|
"hris",
|
|
856
877
|
]);
|
|
857
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Hiring lifecycle of an open role. See `OpenRoleStatus` in `./org-units` for
|
|
881
|
+
* the transition rules. Only `open` and `hiring` are "active" (render a card
|
|
882
|
+
* and count toward `openRoleCount`); `filled`/`closed` are terminal.
|
|
883
|
+
*/
|
|
884
|
+
export const OpenRoleStatusSchema = z.enum([
|
|
885
|
+
"open",
|
|
886
|
+
"hiring",
|
|
887
|
+
"filled",
|
|
888
|
+
"closed",
|
|
889
|
+
]);
|
|
890
|
+
|
|
858
891
|
export const OrgUnitErrorCodeSchema = z.enum([
|
|
859
892
|
"CYCLE_BLOCKED",
|
|
860
893
|
"DEPTH_EXCEEDED",
|
|
@@ -903,6 +936,12 @@ export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
|
|
|
903
936
|
depth: z.number().int().min(1).max(5),
|
|
904
937
|
hasChildren: z.boolean(),
|
|
905
938
|
memberCount: z.number().int().min(0),
|
|
939
|
+
/**
|
|
940
|
+
* Active open roles (`open` + `hiring`) rolled up over this unit's subtree,
|
|
941
|
+
* the same way `memberCount` rolls up real members. Drives the "(N + M open)"
|
|
942
|
+
* count split in the people surfaces. `memberCount` stays real-members-only.
|
|
943
|
+
*/
|
|
944
|
+
openRoleCount: z.number().int().min(0),
|
|
906
945
|
missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
|
|
907
946
|
});
|
|
908
947
|
|
|
@@ -919,6 +958,25 @@ export const OrgUnitMembershipSchema = z.object({
|
|
|
919
958
|
updatedAt: z.string(),
|
|
920
959
|
});
|
|
921
960
|
|
|
961
|
+
/**
|
|
962
|
+
* An open role — a persisted placeholder for the person who will sit in a unit.
|
|
963
|
+
* Carries unit membership (a seat) but never authority. `title` and
|
|
964
|
+
* `targetStartDate` are optional planning attributes. On `fill`, `filledByUserId`
|
|
965
|
+
* points at the seated user and a real `org_unit_membership` is created; the row
|
|
966
|
+
* is kept as staffing history. `targetStartDate` is an ISO date (YYYY-MM-DD).
|
|
967
|
+
*/
|
|
968
|
+
export const OpenRoleSchema = z.object({
|
|
969
|
+
id: z.string().uuid(),
|
|
970
|
+
orgId: z.string().uuid(),
|
|
971
|
+
unitId: z.string().uuid(),
|
|
972
|
+
title: z.string().nullable(),
|
|
973
|
+
targetStartDate: z.string().nullable(),
|
|
974
|
+
status: OpenRoleStatusSchema,
|
|
975
|
+
filledByUserId: z.string().uuid().nullable(),
|
|
976
|
+
createdAt: z.string(),
|
|
977
|
+
updatedAt: z.string(),
|
|
978
|
+
});
|
|
979
|
+
|
|
922
980
|
// ---------------------------------------------------------------------------
|
|
923
981
|
// Authority & Delegation vocabulary
|
|
924
982
|
//
|
|
@@ -1177,6 +1235,49 @@ export const OrgUnitMembershipListResponseSchema = z.object({
|
|
|
1177
1235
|
memberships: z.array(OrgUnitMembershipSchema),
|
|
1178
1236
|
});
|
|
1179
1237
|
|
|
1238
|
+
// ---------------------------------------------------------------------------
|
|
1239
|
+
// Open roles (PRD open-roles)
|
|
1240
|
+
// Request schemas are colocated here (a narrow deviation from ADR-CONT-044, as
|
|
1241
|
+
// with UpdateOrgUnitRequest) so the app's add-role form and the backend
|
|
1242
|
+
// validate against one published shape.
|
|
1243
|
+
// ---------------------------------------------------------------------------
|
|
1244
|
+
|
|
1245
|
+
export const OpenRoleResponseSchema = z.object({ openRole: OpenRoleSchema });
|
|
1246
|
+
|
|
1247
|
+
export const OpenRoleListResponseSchema = z.object({
|
|
1248
|
+
unitId: z.string().uuid(),
|
|
1249
|
+
openRoles: z.array(OpenRoleSchema),
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
/** ISO calendar date (YYYY-MM-DD) — version-agnostic vs `z.string().date()`. */
|
|
1253
|
+
const IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
1254
|
+
message: "must be an ISO date (YYYY-MM-DD)",
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
export const CreateOpenRoleRequestSchema = z.object({
|
|
1258
|
+
title: z.string().min(1).max(255).nullable().optional(),
|
|
1259
|
+
targetStartDate: IsoDateSchema.nullable().optional(),
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
/** Only the non-terminal client transitions are accepted; `fill` is its own
|
|
1263
|
+
* endpoint and `open` is the seeded initial state, never set by the client. */
|
|
1264
|
+
export const UpdateOpenRoleStatusRequestSchema = z.object({
|
|
1265
|
+
status: z.enum(["hiring", "closed"]),
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Fill an open role by seating an existing org member (`userId`) or inviting a
|
|
1270
|
+
* new person by email. Exactly one branch — a discriminated union keyed on `mode`.
|
|
1271
|
+
*/
|
|
1272
|
+
export const FillOpenRoleRequestSchema = z.discriminatedUnion("mode", [
|
|
1273
|
+
z.object({ mode: z.literal("existing"), userId: z.string().uuid() }),
|
|
1274
|
+
z.object({
|
|
1275
|
+
mode: z.literal("invite"),
|
|
1276
|
+
email: z.string().email(),
|
|
1277
|
+
role: z.enum(["admin", "member"]),
|
|
1278
|
+
}),
|
|
1279
|
+
]);
|
|
1280
|
+
|
|
1180
1281
|
/**
|
|
1181
1282
|
* Effective manager of an org unit — a user whose membership (direct or
|
|
1182
1283
|
* inherited from an ancestor) grants the `orgUnit.{id}.manage` capability.
|
|
@@ -1249,6 +1350,14 @@ export type OrgUnitMembershipResponse = z.infer<
|
|
|
1249
1350
|
export type OrgUnitMembershipListResponse = z.infer<
|
|
1250
1351
|
typeof OrgUnitMembershipListResponseSchema
|
|
1251
1352
|
>;
|
|
1353
|
+
export type OpenRole = z.infer<typeof OpenRoleSchema>;
|
|
1354
|
+
export type OpenRoleResponse = z.infer<typeof OpenRoleResponseSchema>;
|
|
1355
|
+
export type OpenRoleListResponse = z.infer<typeof OpenRoleListResponseSchema>;
|
|
1356
|
+
export type CreateOpenRoleRequest = z.infer<typeof CreateOpenRoleRequestSchema>;
|
|
1357
|
+
export type UpdateOpenRoleStatusRequest = z.infer<
|
|
1358
|
+
typeof UpdateOpenRoleStatusRequestSchema
|
|
1359
|
+
>;
|
|
1360
|
+
export type FillOpenRoleRequest = z.infer<typeof FillOpenRoleRequestSchema>;
|
|
1252
1361
|
export type OrgUnitPermissionsEntry = z.infer<
|
|
1253
1362
|
typeof OrgUnitPermissionsEntrySchema
|
|
1254
1363
|
>;
|
package/src/org/types.ts
CHANGED
|
@@ -78,6 +78,17 @@ export interface WorkspaceOverview {
|
|
|
78
78
|
* Mirror of `WorkspaceOverviewSchema.ownerTitle` — keep in lockstep.
|
|
79
79
|
*/
|
|
80
80
|
ownerTitle: string;
|
|
81
|
+
/**
|
|
82
|
+
* Org-configured icon for the `owner` org-chart role (ADR-CONT-070).
|
|
83
|
+
* Server-resolved (`orgs.owner_icon` else "crown"). Mirror of
|
|
84
|
+
* `WorkspaceOverviewSchema.ownerIcon` / `OrgOwnerIconSchema` — keep in lockstep.
|
|
85
|
+
*/
|
|
86
|
+
ownerIcon:
|
|
87
|
+
| "crown"
|
|
88
|
+
| "crown-cross"
|
|
89
|
+
| "person"
|
|
90
|
+
| "person-simple-circle"
|
|
91
|
+
| "medal-military";
|
|
81
92
|
createdAt: string;
|
|
82
93
|
memberCount: number;
|
|
83
94
|
claimable: boolean;
|