← back to Norma
app/api/contacts/counts/route.ts
113 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
/**
* POST /api/contacts/counts
* Batch count related contacts for all clickable profile elements at once.
* Returns counts grouped by type+value so the UI can show badges without
* making individual requests for every skill/school/group/employer.
*
* Body:
* {
* contact_id: string,
* skills: string[],
* schools: string[],
* groups: string[],
* employers: Array<{ company_name: string; start_date: string|null; end_date: string|null }>
* }
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
const body = await request.json();
const { contact_id, skills = [], schools = [], groups = [], employers = [] } = body;
const results: Record<string, number> = {};
await Promise.all([
// Skills
...skills.map(async (skill: string) => {
const r = await query(
`SELECT COUNT(*) AS count FROM contacts c
WHERE c.id != $1::uuid
AND LOWER($2) = ANY(SELECT LOWER(s) FROM unnest(c.skills) s)${orgId ? ' AND c.org_id = $3' : ''}`,
orgId ? [contact_id, skill, orgId] : [contact_id, skill],
);
results[`skill:${skill}`] = parseInt(r.rows[0].count);
}),
// Schools
...schools.map(async (school: string) => {
const r = await query(
`SELECT COUNT(DISTINCT c.id) AS count
FROM contact_education edu
JOIN contacts c ON c.id = edu.contact_id
WHERE c.id != $1::uuid AND LOWER(edu.school) = LOWER($2)${orgId ? ' AND c.org_id = $3' : ''}`,
orgId ? [contact_id, school, orgId] : [contact_id, school],
);
results[`school:${school}`] = parseInt(r.rows[0].count);
}),
// Groups
...groups.map(async (group: string) => {
const r = await query(
`SELECT COUNT(DISTINCT c.id) AS count
FROM contact_groups cg
JOIN contacts c ON c.id = cg.contact_id
WHERE c.id != $1::uuid AND LOWER(cg.group_name) = LOWER($2)${orgId ? ' AND c.org_id = $3' : ''}`,
orgId ? [contact_id, group, orgId] : [contact_id, group],
);
results[`group:${group}`] = parseInt(r.rows[0].count);
}),
// Employers (co-worker counts)
...employers.map(async (emp: { company_name: string; start_date: string | null; end_date: string | null }) => {
const conditions: string[] = [
`LOWER(cwh.company_name) = LOWER($1)`,
`c.id != $2::uuid`,
];
const params: unknown[] = [emp.company_name, contact_id];
let idx = 3;
if (orgId) {
conditions.push(`c.org_id = $${idx}`);
params.push(orgId);
idx++;
}
if (emp.start_date) {
const startYear = new Date(emp.start_date).getFullYear();
conditions.push(`(cwh.end_date IS NULL OR EXTRACT(YEAR FROM cwh.end_date) >= $${idx})`);
params.push(startYear);
idx++;
}
if (emp.end_date) {
const endYear = new Date(emp.end_date).getFullYear();
conditions.push(`(cwh.start_date IS NULL OR EXTRACT(YEAR FROM cwh.start_date) <= $${idx})`);
params.push(endYear);
idx++;
}
const r = await query(
`SELECT COUNT(DISTINCT c.id) AS count
FROM contact_work_history cwh
JOIN contacts c ON c.id = cwh.contact_id
WHERE ${conditions.join(' AND ')}`,
params,
);
results[`employer:${emp.company_name}`] = parseInt(r.rows[0].count);
}),
]);
return NextResponse.json({ counts: results });
} catch (err) {
console.error('[api/contacts/counts] POST error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch counts' }, { status: 500 });
}
}