feat(api): add M5 coverage controls and frontend spec
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
listAssignedShifts,
|
||||
listBusinessAccounts,
|
||||
listBusinessTeamMembers,
|
||||
listBlockedStaff,
|
||||
listCancelledShifts,
|
||||
listCertificates,
|
||||
listCostCenters,
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
listStaffAvailability,
|
||||
listStaffBankAccounts,
|
||||
listStaffBenefits,
|
||||
listStaffBenefitHistory,
|
||||
listTodayShifts,
|
||||
listVendorRoles,
|
||||
listVendors,
|
||||
@@ -91,6 +93,7 @@ const defaultQueryService = {
|
||||
listAssignedShifts,
|
||||
listBusinessAccounts,
|
||||
listBusinessTeamMembers,
|
||||
listBlockedStaff,
|
||||
listCancelledShifts,
|
||||
listCertificates,
|
||||
listCostCenters,
|
||||
@@ -116,6 +119,7 @@ const defaultQueryService = {
|
||||
listStaffAvailability,
|
||||
listStaffBankAccounts,
|
||||
listStaffBenefits,
|
||||
listStaffBenefitHistory,
|
||||
listTodayShifts,
|
||||
listVendorRoles,
|
||||
listVendors,
|
||||
@@ -253,6 +257,15 @@ export function createMobileQueryRouter(queryService = defaultQueryService) {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/coverage/blocked-staff', requireAuth, requirePolicy('coverage.read', 'coverage'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listBlockedStaff(req.actor.uid);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/client/hubs', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listHubs(req.actor.uid);
|
||||
@@ -622,6 +635,15 @@ export function createMobileQueryRouter(queryService = defaultQueryService) {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/benefits/history', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listStaffBenefitHistory(req.actor.uid, req.query);
|
||||
return res.status(200).json({ items, requestId: req.requestId });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/staff/profile/time-card', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => {
|
||||
try {
|
||||
const items = await queryService.listTimeCardEntries(req.actor.uid, req.query);
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user