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))
|
||||
}
|
||||
|
||||
// --- Members (implemented in Slice 2) ---
|
||||
|
||||
export async function fetchNaptaProjectMembers(_projectId: string): Promise<Member[]> {
|
||||
throw new Error("Napta member resolution not implemented yet (Slice 2)")
|
||||
// Prefer a flat `<name>_id` attribute, else the relationship's resource id.
|
||||
function relatedId(resource: JsonApiResource, name: string): string | null {
|
||||
const flat = resource.attributes[`${name}_id`]
|
||||
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