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:
Julien Calixte
2026-06-26 16:20:51 +01:00
parent 9ded10f68e
commit e47302218b

View File

@@ -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),
)
} }