@@ -1278,6 +1278,9 @@ route("GET", "/admin/users", handleAdminListUsers);
12781278route ( "GET" , "/admin/jobs" , handleAdminListJobs ) ;
12791279route ( "GET" , "/admin/audit-logs" , handleAdminAuditLogs ) ;
12801280route ( "POST" , "/admin/test-email" , handleAdminTestEmail ) ;
1281+ route ( "GET" , "/admin/interview-stats" , handleAdminInterviewStats ) ;
1282+ route ( "PATCH" , "/admin/companies/:id/block" , handleAdminToggleCompanyBlock ) ;
1283+ route ( "PATCH" , "/admin/companies/:id/interview-block" , handleAdminToggleInterviewBlock ) ;
12811284
12821285// ── Attendance ──
12831286route ( "POST" , "/attendance/clock-in" , handleClockIn ) ;
@@ -6385,6 +6388,36 @@ async function handleCreateInterview(request, env, ctx) {
63856388 throw new ValidationError ( "candidate_name and candidate_email are required" ) ;
63866389 }
63876390
6391+ // ── Enforce company-level blocks ──
6392+ const companyId = body . company_id || ctx . tenantId ;
6393+ if ( companyId && companyId !== "default" ) {
6394+ try {
6395+ const compRes = await fetch (
6396+ `${ env . SUPABASE_URL } /rest/v1/companies?id=eq.${ companyId } &select=is_blocked,interviews_blocked` ,
6397+ {
6398+ headers : {
6399+ apikey : env . SUPABASE_SERVICE_KEY ,
6400+ Authorization : `Bearer ${ env . SUPABASE_SERVICE_KEY } ` ,
6401+ } ,
6402+ } ,
6403+ ) ;
6404+ if ( compRes . ok ) {
6405+ const companies = await compRes . json ( ) ;
6406+ if ( companies . length > 0 ) {
6407+ if ( companies [ 0 ] . is_blocked ) {
6408+ throw new ForbiddenError ( "This company account has been blocked by the platform administrator." ) ;
6409+ }
6410+ if ( companies [ 0 ] . interviews_blocked ) {
6411+ throw new ForbiddenError ( "Interview access has been restricted for this company. Please contact the platform administrator." ) ;
6412+ }
6413+ }
6414+ }
6415+ } catch ( e ) {
6416+ if ( e instanceof ForbiddenError ) throw e ;
6417+ console . warn ( "[Interview] Company block check failed:" , e . message ) ;
6418+ }
6419+ }
6420+
63886421 const token = body . token || Array . from ( crypto . getRandomValues ( new Uint8Array ( 24 ) ) )
63896422 . map ( b => b . toString ( 36 ) . padStart ( 2 , "0" ) ) . join ( "" ) . slice ( 0 , 32 ) ;
63906423
@@ -6714,6 +6747,154 @@ async function handleAdminTestEmail(request, env, ctx) {
67146747 return apiResponse ( { sent : true , to } ) ;
67156748}
67166749
6750+ /**
6751+ * GET /admin/interview-stats — Per-company interview usage stats (super_admin only).
6752+ * Returns each company's total interviews, completed, pending, and date of last interview.
6753+ */
6754+ async function handleAdminInterviewStats ( request , env , ctx ) {
6755+ requireRole ( ctx , "super_admin" , "superadmin" ) ;
6756+
6757+ // Fetch all companies
6758+ const compRes = await fetch (
6759+ `${ env . SUPABASE_URL } /rest/v1/companies?select=id,name,is_blocked,interviews_blocked` ,
6760+ {
6761+ headers : {
6762+ apikey : env . SUPABASE_SERVICE_KEY ,
6763+ Authorization : `Bearer ${ env . SUPABASE_SERVICE_KEY } ` ,
6764+ } ,
6765+ } ,
6766+ ) ;
6767+ const companies = compRes . ok ? await compRes . json ( ) : [ ] ;
6768+
6769+ // Fetch all interviews with company_id
6770+ const intRes = await fetch (
6771+ `${ env . SUPABASE_URL } /rest/v1/interviews?select=id,company_id,status,created_at&order=created_at.desc` ,
6772+ {
6773+ headers : {
6774+ apikey : env . SUPABASE_SERVICE_KEY ,
6775+ Authorization : `Bearer ${ env . SUPABASE_SERVICE_KEY } ` ,
6776+ } ,
6777+ } ,
6778+ ) ;
6779+ const interviews = intRes . ok ? await intRes . json ( ) : [ ] ;
6780+
6781+ // Build per-company stats
6782+ const statsMap = { } ;
6783+ for ( const c of companies ) {
6784+ statsMap [ c . id ] = {
6785+ company_id : c . id ,
6786+ company_name : c . name ,
6787+ is_blocked : c . is_blocked || false ,
6788+ interviews_blocked : c . interviews_blocked || false ,
6789+ total_interviews : 0 ,
6790+ completed : 0 ,
6791+ pending : 0 ,
6792+ scheduled : 0 ,
6793+ expired : 0 ,
6794+ last_interview_at : null ,
6795+ } ;
6796+ }
6797+
6798+ for ( const iv of interviews ) {
6799+ const cid = iv . company_id ;
6800+ if ( ! statsMap [ cid ] ) {
6801+ statsMap [ cid ] = {
6802+ company_id : cid ,
6803+ company_name : "Unknown" ,
6804+ is_blocked : false ,
6805+ interviews_blocked : false ,
6806+ total_interviews : 0 ,
6807+ completed : 0 ,
6808+ pending : 0 ,
6809+ scheduled : 0 ,
6810+ expired : 0 ,
6811+ last_interview_at : null ,
6812+ } ;
6813+ }
6814+ statsMap [ cid ] . total_interviews ++ ;
6815+ if ( iv . status === "completed" ) statsMap [ cid ] . completed ++ ;
6816+ else if ( iv . status === "pending" ) statsMap [ cid ] . pending ++ ;
6817+ else if ( iv . status === "scheduled" ) statsMap [ cid ] . scheduled ++ ;
6818+ else if ( iv . status === "expired" ) statsMap [ cid ] . expired ++ ;
6819+ if ( ! statsMap [ cid ] . last_interview_at ) {
6820+ statsMap [ cid ] . last_interview_at = iv . created_at ;
6821+ }
6822+ }
6823+
6824+ const stats = Object . values ( statsMap ) . sort ( ( a , b ) => b . total_interviews - a . total_interviews ) ;
6825+ return apiResponse ( {
6826+ stats,
6827+ total_interviews : interviews . length ,
6828+ total_companies : companies . length ,
6829+ } ) ;
6830+ }
6831+
6832+ /**
6833+ * PATCH /admin/companies/:id/block — Toggle company blocked status (super_admin only).
6834+ */
6835+ async function handleAdminToggleCompanyBlock ( request , env , ctx , [ companyId ] ) {
6836+ requireRole ( ctx , "super_admin" , "superadmin" ) ;
6837+
6838+ const body = await safeJson ( request ) ;
6839+ const blocked = ! ! body . blocked ;
6840+
6841+ const res = await fetch (
6842+ `${ env . SUPABASE_URL } /rest/v1/companies?id=eq.${ companyId } ` ,
6843+ {
6844+ method : "PATCH" ,
6845+ headers : {
6846+ "Content-Type" : "application/json" ,
6847+ apikey : env . SUPABASE_SERVICE_KEY ,
6848+ Authorization : `Bearer ${ env . SUPABASE_SERVICE_KEY } ` ,
6849+ Prefer : "return=representation" ,
6850+ } ,
6851+ body : JSON . stringify ( { is_blocked : blocked } ) ,
6852+ } ,
6853+ ) ;
6854+
6855+ if ( ! res . ok ) throw new AppError ( "Failed to update company" , res . status ) ;
6856+ const updated = await res . json ( ) ;
6857+
6858+ await audit ( env , ctx , blocked ? "admin.company_blocked" : "admin.company_unblocked" , "companies" , companyId , {
6859+ blocked,
6860+ } ) ;
6861+
6862+ return apiResponse ( { company : updated [ 0 ] || { id : companyId , is_blocked : blocked } } ) ;
6863+ }
6864+
6865+ /**
6866+ * PATCH /admin/companies/:id/interview-block — Toggle interview access for a company (super_admin only).
6867+ */
6868+ async function handleAdminToggleInterviewBlock ( request , env , ctx , [ companyId ] ) {
6869+ requireRole ( ctx , "super_admin" , "superadmin" ) ;
6870+
6871+ const body = await safeJson ( request ) ;
6872+ const blocked = ! ! body . blocked ;
6873+
6874+ const res = await fetch (
6875+ `${ env . SUPABASE_URL } /rest/v1/companies?id=eq.${ companyId } ` ,
6876+ {
6877+ method : "PATCH" ,
6878+ headers : {
6879+ "Content-Type" : "application/json" ,
6880+ apikey : env . SUPABASE_SERVICE_KEY ,
6881+ Authorization : `Bearer ${ env . SUPABASE_SERVICE_KEY } ` ,
6882+ Prefer : "return=representation" ,
6883+ } ,
6884+ body : JSON . stringify ( { interviews_blocked : blocked } ) ,
6885+ } ,
6886+ ) ;
6887+
6888+ if ( ! res . ok ) throw new AppError ( "Failed to update company interview access" , res . status ) ;
6889+ const updated = await res . json ( ) ;
6890+
6891+ await audit ( env , ctx , blocked ? "admin.interviews_blocked" : "admin.interviews_unblocked" , "companies" , companyId , {
6892+ interviews_blocked : blocked ,
6893+ } ) ;
6894+
6895+ return apiResponse ( { company : updated [ 0 ] || { id : companyId , interviews_blocked : blocked } } ) ;
6896+ }
6897+
67176898// ===============================================================
67186899// § ATTENDANCE — Clock In / Clock Out / Records
67196900// ===============================================================
0 commit comments