From fd5c844108cc87bb924766fe96bc1b8a1d1d5928 Mon Sep 17 00:00:00 2001 From: Brandon Korous Date: Fri, 10 Apr 2026 16:26:35 -0700 Subject: [PATCH 1/3] feat: Add comprehensive management documentation for various admin features - Introduced detailed documentation for Candidates Management, Chat Moderation, Companies Management, Content Management, Dashboard & Analytics, Decision Log, Firms Management, Fraud Detection & Trust Management, Jobs Management, Matches Management, Notifications Management, Organizations Management, Payouts & Billing Management, Placements Management, Recruiters Management, Settings Management, Support Management, and Users Management. - Each document outlines the current state, existing features, missing features categorized by priority, and implementation notes for future development. --- .../[id]/components/recruiter-companies.tsx | 76 +++++ .../components/recruiter-detail-actions.tsx | 126 +++++++++ .../[id]/components/recruiter-edit-modal.tsx | 232 +++++++++++++++ .../[id]/components/recruiter-overview.tsx | 263 ++++++++++++++++++ .../src/app/secure/recruiters/[id]/page.tsx | 163 +++++++++++ .../recruiters/components/recruiter-table.tsx | 3 + docs/admin/README.md | 132 +++++++++ docs/admin/applications.md | 47 ++++ docs/admin/assignments.md | 41 +++ docs/admin/automation.md | 47 ++++ docs/admin/candidates.md | 46 +++ docs/admin/chat-moderation.md | 44 +++ docs/admin/companies.md | 45 +++ docs/admin/content.md | 64 +++++ docs/admin/dashboard-analytics.md | 73 +++++ docs/admin/decision-log.md | 41 +++ docs/admin/firms.md | 45 +++ docs/admin/fraud-trust.md | 67 +++++ docs/admin/jobs.md | 51 ++++ docs/admin/matches.md | 48 ++++ docs/admin/notifications.md | 40 +++ docs/admin/organizations.md | 39 +++ docs/admin/payouts-billing.md | 68 +++++ docs/admin/placements.md | 45 +++ docs/admin/recruiters.md | 49 ++++ docs/admin/settings.md | 51 ++++ docs/admin/support.md | 67 +++++ docs/admin/users.md | 46 +++ .../src/v3/admin/repository.ts | 32 +++ .../network-service/src/v3/admin/routes.ts | 27 +- .../network-service/src/v3/admin/service.ts | 10 + .../network-service/src/v3/admin/types.ts | 18 ++ 32 files changed, 2144 insertions(+), 2 deletions(-) create mode 100644 apps/admin/src/app/secure/recruiters/[id]/components/recruiter-companies.tsx create mode 100644 apps/admin/src/app/secure/recruiters/[id]/components/recruiter-detail-actions.tsx create mode 100644 apps/admin/src/app/secure/recruiters/[id]/components/recruiter-edit-modal.tsx create mode 100644 apps/admin/src/app/secure/recruiters/[id]/components/recruiter-overview.tsx create mode 100644 apps/admin/src/app/secure/recruiters/[id]/page.tsx create mode 100644 docs/admin/README.md create mode 100644 docs/admin/applications.md create mode 100644 docs/admin/assignments.md create mode 100644 docs/admin/automation.md create mode 100644 docs/admin/candidates.md create mode 100644 docs/admin/chat-moderation.md create mode 100644 docs/admin/companies.md create mode 100644 docs/admin/content.md create mode 100644 docs/admin/dashboard-analytics.md create mode 100644 docs/admin/decision-log.md create mode 100644 docs/admin/firms.md create mode 100644 docs/admin/fraud-trust.md create mode 100644 docs/admin/jobs.md create mode 100644 docs/admin/matches.md create mode 100644 docs/admin/notifications.md create mode 100644 docs/admin/organizations.md create mode 100644 docs/admin/payouts-billing.md create mode 100644 docs/admin/placements.md create mode 100644 docs/admin/recruiters.md create mode 100644 docs/admin/settings.md create mode 100644 docs/admin/support.md create mode 100644 docs/admin/users.md diff --git a/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-companies.tsx b/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-companies.tsx new file mode 100644 index 000000000..db27eb2de --- /dev/null +++ b/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-companies.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { AdminDataTable, type Column } from '@/components/shared'; + +type CompanyRelation = { + id: string; + status: string; + relationship_type: string; + company: { id: string; name: string; logo_url: string | null } | null; + created_at: string; +}; + +const STATUS_BADGE: Record = { + active: 'badge-success', + pending: 'badge-warning', + declined: 'badge-error', + terminated: 'badge-ghost', +}; + +const COLUMNS: Column[] = [ + { + key: 'company', + label: 'Company', + render: (item) => ( +
+ {item.company?.logo_url && ( + + )} + {item.company?.name ?? '—'} +
+ ), + }, + { + key: 'relationship_type', + label: 'Type', + render: (item) => ( + {item.relationship_type} + ), + }, + { + key: 'status', + label: 'Status', + render: (item) => ( + + {item.status} + + ), + }, + { + key: 'created_at', + label: 'Since', + render: (item) => ( + + {new Date(item.created_at).toLocaleDateString()} + + ), + }, +]; + +type Props = { + companies: CompanyRelation[]; +}; + +export function RecruiterCompanies({ companies }: Props) { + return ( +
+ +
+ ); +} diff --git a/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-detail-actions.tsx b/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-detail-actions.tsx new file mode 100644 index 000000000..4d670c2aa --- /dev/null +++ b/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-detail-actions.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useState } from 'react'; +import { useAuth } from '@clerk/nextjs'; +import { AdminConfirmModal } from '@/components/shared'; +import { AdminApiClient } from '@/lib/api-client'; +import { useAdminToast } from '@/hooks/use-admin-toast'; + +type Status = 'pending' | 'active' | 'suspended' | 'inactive'; + +type Props = { + recruiterId: string; + currentStatus: Status; + onSuccess: () => void; +}; + +type ActionConfig = { + label: string; + newStatus: Status; + title: string; + message: string; + variant: 'primary' | 'warning' | 'error'; + btnClass: string; +}; + +function getActions(status: Status): ActionConfig[] { + const actions: ActionConfig[] = []; + + if (status === 'pending' || status === 'suspended') { + actions.push({ + label: 'Approve', + newStatus: 'active', + title: 'Approve Recruiter', + message: 'This recruiter will be activated and can access the platform.', + variant: 'primary', + btnClass: 'btn-primary', + }); + } + + if (status === 'active') { + actions.push({ + label: 'Suspend', + newStatus: 'suspended', + title: 'Suspend Recruiter', + message: 'This recruiter will be suspended and lose platform access.', + variant: 'warning', + btnClass: 'btn-warning', + }); + } + + if (status !== 'inactive') { + actions.push({ + label: 'Deactivate', + newStatus: 'inactive', + title: 'Deactivate Recruiter', + message: 'This recruiter will be deactivated. This is a soft-delete.', + variant: 'error', + btnClass: 'btn-error btn-outline', + }); + } + + return actions; +} + +export function RecruiterActions({ recruiterId, currentStatus, onSuccess }: Props) { + const { getToken } = useAuth(); + const toast = useAdminToast(); + const [pending, setPending] = useState(null); + const [loading, setLoading] = useState(false); + + const actions = getActions(currentStatus); + + async function handleConfirm() { + if (!pending) return; + setLoading(true); + try { + const token = await getToken(); + if (!token) throw new Error('Not authenticated'); + const client = new AdminApiClient(token); + await client.patch(`/network/admin/recruiters/${recruiterId}/status`, { + status: pending.newStatus, + }); + toast.success(`Recruiter ${pending.label.toLowerCase()}d successfully`); + onSuccess(); + } catch { + toast.error('Failed to update recruiter status'); + } finally { + setLoading(false); + setPending(null); + } + } + + if (actions.length === 0) return null; + + return ( + <> +
+
+ +
+
    + {actions.map((action) => ( +
  • + +
  • + ))} +
+
+ + {pending && ( + setPending(null)} + onConfirm={handleConfirm} + title={pending.title} + message={pending.message} + confirmLabel={pending.label} + confirmVariant={pending.variant} + loading={loading} + /> + )} + + ); +} diff --git a/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-edit-modal.tsx b/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-edit-modal.tsx new file mode 100644 index 000000000..242000638 --- /dev/null +++ b/apps/admin/src/app/secure/recruiters/[id]/components/recruiter-edit-modal.tsx @@ -0,0 +1,232 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { useAuth } from '@clerk/nextjs'; +import { AdminApiClient } from '@/lib/api-client'; +import { useAdminToast } from '@/hooks/use-admin-toast'; +import type { RecruiterDetail } from '../page'; + +type Props = { + recruiter: RecruiterDetail; + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +}; + +export function RecruiterEditModal({ recruiter, isOpen, onClose, onSuccess }: Props) { + const dialogRef = useRef(null); + const { getToken } = useAuth(); + const toast = useAdminToast(); + const [saving, setSaving] = useState(false); + + const [form, setForm] = useState({ + bio: recruiter.bio ?? '', + tagline: recruiter.tagline ?? '', + location: recruiter.location ?? '', + phone: recruiter.phone ?? '', + years_experience: recruiter.years_experience ?? '', + industries: (recruiter.industries ?? []).join(', '), + specialties: (recruiter.specialties ?? []).join(', '), + candidate_recruiter: recruiter.candidate_recruiter, + company_recruiter: recruiter.company_recruiter, + marketplace_enabled: recruiter.marketplace_enabled, + marketplace_visibility: recruiter.marketplace_visibility ?? 'public', + }); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + if (isOpen) dialog.showModal(); + else dialog.close(); + }, [isOpen]); + + useEffect(() => { + setForm({ + bio: recruiter.bio ?? '', + tagline: recruiter.tagline ?? '', + location: recruiter.location ?? '', + phone: recruiter.phone ?? '', + years_experience: recruiter.years_experience ?? '', + industries: (recruiter.industries ?? []).join(', '), + specialties: (recruiter.specialties ?? []).join(', '), + candidate_recruiter: recruiter.candidate_recruiter, + company_recruiter: recruiter.company_recruiter, + marketplace_enabled: recruiter.marketplace_enabled, + marketplace_visibility: recruiter.marketplace_visibility ?? 'public', + }); + }, [recruiter]); + + function parseList(value: string): string[] { + return value.split(',').map(s => s.trim()).filter(Boolean); + } + + async function handleSave() { + setSaving(true); + try { + const token = await getToken(); + if (!token) throw new Error('Not authenticated'); + const client = new AdminApiClient(token); + await client.patch(`/network/admin/recruiters/${recruiter.id}`, { + bio: form.bio || null, + tagline: form.tagline || null, + location: form.location || null, + phone: form.phone || null, + years_experience: form.years_experience ? Number(form.years_experience) : null, + industries: parseList(form.industries), + specialties: parseList(form.specialties), + candidate_recruiter: form.candidate_recruiter, + company_recruiter: form.company_recruiter, + marketplace_enabled: form.marketplace_enabled, + marketplace_visibility: form.marketplace_visibility, + }); + toast.success('Recruiter profile updated'); + onSuccess(); + } catch { + toast.error('Failed to update recruiter'); + } finally { + setSaving(false); + } + } + + return ( + +
+

Edit Recruiter Profile

+ +
+
+ Tagline + setForm(f => ({ ...f, tagline: e.target.value }))} + /> +
+ +
+ Bio +