feat(napta): resolve project Members (real+active, role, sorted)
Filter user_project by project + simulated=false, include user, drop inactive people, dedupe per user, map user_position -> Role, sort by last name.
This commit is contained in:
@@ -137,8 +137,68 @@ export async function fetchNaptaProjects(): Promise<Project[]> {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Members (implemented in Slice 2) ---
|
// Prefer a flat `<name>_id` attribute, else the relationship's resource id.
|
||||||
|
function relatedId(resource: JsonApiResource, name: string): string | null {
|
||||||
export async function fetchNaptaProjectMembers(_projectId: string): Promise<Member[]> {
|
const flat = resource.attributes[`${name}_id`]
|
||||||
throw new Error("Napta member resolution not implemented yet (Slice 2)")
|
if (flat != null) return String(flat)
|
||||||
|
return relId(resource, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Positions (Role lookup), cached for the process lifetime ---
|
||||||
|
|
||||||
|
let positionMapCache: Map<string, string> | null = null
|
||||||
|
|
||||||
|
async function getPositionMap(): Promise<Map<string, string>> {
|
||||||
|
if (positionMapCache) return positionMapCache
|
||||||
|
const { data } = await naptaGetAll("/user_position", {})
|
||||||
|
const map = new Map<string, string>()
|
||||||
|
for (const p of data) map.set(p.id, attr(p, "name"))
|
||||||
|
positionMapCache = map
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Members ---
|
||||||
|
|
||||||
|
export async function fetchNaptaProjectMembers(projectId: string): Promise<Member[]> {
|
||||||
|
const [{ data, included }, positions] = await Promise.all([
|
||||||
|
naptaGetAll("/user_project", {
|
||||||
|
// Real staffing on this Project only (drop simulated/forecast scenarios).
|
||||||
|
filters: [
|
||||||
|
{ name: "project_id", op: "eq", val: projectId },
|
||||||
|
{ name: "simulated", op: "eq", val: false },
|
||||||
|
],
|
||||||
|
include: ["user"],
|
||||||
|
}),
|
||||||
|
getPositionMap(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const userById = new Map<string, JsonApiResource>()
|
||||||
|
for (const inc of included) {
|
||||||
|
if (inc.type === "user") userById.set(inc.id, inc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// One Member per user, even if they have several assignments on the Project.
|
||||||
|
const byUserId = new Map<string, Member>()
|
||||||
|
for (const up of data) {
|
||||||
|
const userId = relatedId(up, "user")
|
||||||
|
if (!userId || byUserId.has(userId)) continue
|
||||||
|
const user = userById.get(userId)
|
||||||
|
if (!user) continue
|
||||||
|
if (user.attributes.active === false) continue // drop departed people
|
||||||
|
const positionId = relatedId(user, "user_position")
|
||||||
|
byUserId.set(userId, {
|
||||||
|
id: userId,
|
||||||
|
firstName: attr(user, "first_name"),
|
||||||
|
lastName: attr(user, "last_name"),
|
||||||
|
role: (positionId && positions.get(positionId)) || "",
|
||||||
|
email: attr(user, "email"),
|
||||||
|
imageUrl: null,
|
||||||
|
slackMatched: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...byUserId.values()].sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.lastName.localeCompare(b.lastName) || a.firstName.localeCompare(b.firstName),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user