feat(api): add M5 coverage controls and frontend spec

This commit is contained in:
zouantchaw
2026-03-18 08:18:50 +01:00
parent 008dd7efb1
commit 32f6cd55c8
14 changed files with 894 additions and 8 deletions

View File

@@ -52,6 +52,13 @@ function metadataBoolean(metadata, key, fallback = false) {
return fallback;
}
function membershipDisplayName(row) {
const firstName = row?.firstName || row?.metadata?.firstName || null;
const lastName = row?.lastName || row?.metadata?.lastName || null;
const fullName = [firstName, lastName].filter(Boolean).join(' ').trim();
return fullName || row?.name || row?.displayName || row?.email || row?.invitedEmail || null;
}
function getProfileCompletionFromMetadata(staffRow) {
const metadata = staffRow?.metadata || {};
const [firstName, ...lastParts] = (staffRow?.fullName || '').trim().split(/\s+/);
@@ -513,10 +520,20 @@ export async function listHubManagers(actorUid, hubId) {
hm.id AS "managerAssignmentId",
bm.id AS "businessMembershipId",
u.id AS "managerId",
COALESCE(u.display_name, u.email) AS name
u.display_name AS "displayName",
u.email,
bm.invited_email AS "invitedEmail",
bm.membership_status AS "membershipStatus",
bm.metadata,
COALESCE(
NULLIF(TRIM(CONCAT_WS(' ', bm.metadata->>'firstName', bm.metadata->>'lastName')), ''),
u.display_name,
u.email,
bm.invited_email
) AS name
FROM hub_managers hm
JOIN business_memberships bm ON bm.id = hm.business_membership_id
JOIN users u ON u.id = bm.user_id
LEFT JOIN users u ON u.id = bm.user_id
WHERE hm.tenant_id = $1
AND hm.hub_id = $2
ORDER BY name ASC
@@ -1323,6 +1340,35 @@ export async function listStaffBenefits(actorUid) {
return result.rows;
}
export async function listStaffBenefitHistory(actorUid, { limit, offset } = {}) {
const context = await requireStaffContext(actorUid);
const safeLimit = parseLimit(limit, 20, 100);
const safeOffset = Number.isFinite(Number(offset)) && Number(offset) >= 0 ? Number(offset) : 0;
const result = await query(
`
SELECT
id AS "historyId",
benefit_id AS "benefitId",
benefit_type AS "benefitType",
title,
status,
effective_at AS "effectiveAt",
ended_at AS "endedAt",
tracked_hours AS "trackedHours",
target_hours AS "targetHours",
notes,
metadata
FROM staff_benefit_history
WHERE tenant_id = $1
AND staff_id = $2
ORDER BY effective_at DESC, created_at DESC
LIMIT $3 OFFSET $4
`,
[context.tenant.tenantId, context.staff.staffId, safeLimit, safeOffset]
);
return result.rows;
}
export async function listCoreTeam(actorUid) {
const context = await requireClientContext(actorUid);
const result = await query(
@@ -1345,6 +1391,28 @@ export async function listCoreTeam(actorUid) {
return result.rows;
}
export async function listBlockedStaff(actorUid) {
const context = await requireClientContext(actorUid);
const result = await query(
`
SELECT
st.id AS "staffId",
st.full_name AS "fullName",
st.primary_role AS "primaryRole",
sb.reason,
sb.issue_flags AS "issueFlags",
sb.created_at AS "blockedAt"
FROM staff_blocks sb
JOIN staffs st ON st.id = sb.staff_id
WHERE sb.tenant_id = $1
AND sb.business_id = $2
ORDER BY sb.created_at DESC, st.full_name ASC
`,
[context.tenant.tenantId, context.business.businessId]
);
return result.rows;
}
export async function getOrderReorderPreview(actorUid, orderId) {
const context = await requireClientContext(actorUid);
const result = await query(
@@ -1405,19 +1473,29 @@ export async function listBusinessTeamMembers(actorUid) {
SELECT
bm.id AS "businessMembershipId",
u.id AS "userId",
COALESCE(u.display_name, u.email) AS name,
u.display_name AS "displayName",
u.email,
bm.business_role AS role
bm.invited_email AS "invitedEmail",
bm.business_role AS role,
bm.membership_status AS "membershipStatus",
bm.metadata
FROM business_memberships bm
JOIN users u ON u.id = bm.user_id
LEFT JOIN users u ON u.id = bm.user_id
WHERE bm.tenant_id = $1
AND bm.business_id = $2
AND bm.membership_status = 'ACTIVE'
ORDER BY name ASC
AND bm.membership_status IN ('ACTIVE', 'INVITED')
ORDER BY COALESCE(u.display_name, u.email, bm.invited_email) ASC
`,
[context.tenant.tenantId, context.business.businessId]
);
return result.rows;
return result.rows.map((row) => ({
businessMembershipId: row.businessMembershipId,
userId: row.userId,
name: membershipDisplayName(row),
email: row.email || row.invitedEmail || null,
role: row.role,
membershipStatus: row.membershipStatus,
}));
}
export async function getReportSummary(actorUid, { startDate, endDate }) {