1+ import React from "react" ;
2+ import { Check , X } from "lucide-react" ;
3+
4+ type Report = {
5+ id : string ;
6+ index : number ;
7+ weighted_index : number ;
8+ technical_score : number ;
9+ domain_score : number ;
10+ career_score : number ;
11+ cultural_score : number ;
12+ technical_confidence : number ;
13+ domain_confidence : number ;
14+ career_confidence : number ;
15+ cultural_confidence : number ;
16+ pros : string ;
17+ cons : string ;
18+ hiring_advice : string ;
19+ candidate_advice : string ;
20+ concern_tags : string [ ] ;
21+ date_created : string ;
22+ submission ?: {
23+ job_description ?: {
24+ id : string ;
25+ role_name ?: string ;
26+ company_name ?: string ;
27+ backfill_status ?: string ;
28+ } ;
29+ cv_file ?: string ;
30+ user_created ?: { first_name ?: string ; last_name ?: string } ;
31+ } ;
32+ } ;
33+
34+ interface PrintableReportProps {
35+ report : Report ;
36+ reportId : string ;
37+ candidateName : string ;
38+ roleName : string ;
39+ companyName : string ;
40+ backfillStatus : string ;
41+ }
42+
43+ function fmtMinutes ( iso : string ) {
44+ const d = new Date ( iso ) ;
45+ if ( Number . isNaN ( d . getTime ( ) ) ) return "—" ;
46+ const p = ( n : number ) => String ( n ) . padStart ( 2 , "0" ) ;
47+ return `${ d . getFullYear ( ) } -${ p ( d . getMonth ( ) + 1 ) } -${ p ( d . getDate ( ) ) } ${ p (
48+ d . getHours ( )
49+ ) } :${ p ( d . getMinutes ( ) ) } `;
50+ }
51+
52+ const PrintableReport = React . forwardRef < HTMLDivElement , PrintableReportProps > (
53+ ( { report, reportId, candidateName, roleName, companyName, backfillStatus } , ref ) => {
54+ const prosList =
55+ report ?. pros
56+ ?. split ( "\n" )
57+ . map ( ( l ) => l . replace ( / ^ \s * - \s * / , "" ) . trim ( ) )
58+ . filter ( Boolean ) || [ ] ;
59+
60+ const consList =
61+ report ?. cons
62+ ?. split ( "\n" )
63+ . map ( ( l ) => l . replace ( / ^ \s * - \s * / , "" ) . trim ( ) )
64+ . filter ( Boolean ) || [ ] ;
65+
66+ const hiring_advices =
67+ report ?. hiring_advice
68+ ?. split ( "\n" )
69+ . map ( ( l ) => l . replace ( / ^ \s * - \s * / , "" ) . trim ( ) )
70+ . filter ( Boolean ) || [ ] ;
71+
72+ const candidate_advices =
73+ report ?. candidate_advice
74+ ?. split ( "\n" )
75+ . map ( ( l ) => l . replace ( / ^ \s * - \s * / , "" ) . trim ( ) )
76+ . filter ( Boolean ) || [ ] ;
77+
78+ const ProgressBar = ( { value } : { value : number } ) => (
79+ < div className = "w-full bg-gray-200 rounded-full h-2" >
80+ < div
81+ className = "bg-black h-2 rounded-full"
82+ style = { { width : `${ Math . min ( 100 , Math . max ( 0 , value ) ) } %` } }
83+ />
84+ </ div >
85+ ) ;
86+
87+ return (
88+ < div ref = { ref } className = "max-w-4xl mx-auto p-4 bg-white text-black" >
89+ < style >
90+ { `
91+ @media print {
92+ body { print-color-adjust: exact; }
93+ .page-break { page-break-before: always; }
94+ .no-break { break-inside: avoid; }
95+ }
96+ .print-grid-2 {
97+ display: grid;
98+ grid-template-columns: 1fr 1fr;
99+ gap: 1.5rem;
100+ }
101+ ` }
102+ </ style >
103+
104+ { /* Header */ }
105+ < div className = "text-center mb-4 border-b pb-3" >
106+ < div className = "flex items-center justify-center mb-2" >
107+ < img src = "/apple-icon.png" alt = "Bounteer Logo" className = "h-6 w-auto mr-2" />
108+ < span className = "font-bold text-gray-800" style = { { fontSize : '14px' } } > Bounteer</ span >
109+ </ div >
110+ < h1 className = "font-bold mb-2" style = { { fontSize : '20px' } } > Role Fit Index Report</ h1 >
111+ < div className = "space-y-1" >
112+ < p style = { { fontSize : '11px' } } >
113+ < strong > Candidate:</ strong > { candidateName } · < strong > Role:</ strong > { roleName } @ { companyName }
114+ </ p >
115+ { backfillStatus && backfillStatus . toLowerCase ( ) !== "success" && (
116+ < p className = "text-red-600 font-medium" style = { { fontSize : '11px' } } >
117+ Status: { backfillStatus }
118+ </ p >
119+ ) }
120+ < p className = "text-gray-600" style = { { fontSize : '9px' } } >
121+ Report ID: { reportId } · Created: { fmtMinutes ( report . date_created ) }
122+ </ p >
123+ </ div >
124+ </ div >
125+
126+ { /* Indexes */ }
127+ < div className = "print-grid-2 mb-4 no-break" >
128+ < div className = "rounded-lg bg-gray-50 p-3 text-center" >
129+ < h2 className = "font-semibold mb-1" style = { { fontSize : '14px' } } > Role Fit Index</ h2 >
130+ < p className = "font-bold text-black" style = { { fontSize : '20px' } } > { report . index } /100</ p >
131+ </ div >
132+ < div className = "rounded-lg bg-gray-50 p-3 text-center" >
133+ < h2 className = "font-semibold mb-1" style = { { fontSize : '14px' } } > Weighted Role Fit Index</ h2 >
134+ < p className = "font-bold text-black" style = { { fontSize : '20px' } } > { report . weighted_index } /100</ p >
135+ </ div >
136+ </ div >
137+
138+ { /* Breakdown Scores */ }
139+ < div className = "mb-4 no-break" >
140+ < h2 className = "font-semibold mb-2" style = { { fontSize : '16px' } } > Breakdown Scores</ h2 >
141+ < div className = "rounded-lg bg-gray-50 p-3 space-y-2" >
142+ { [
143+ {
144+ label : "Technical Proficiency" ,
145+ score : report . technical_score ,
146+ confidence : report . technical_confidence ,
147+ } ,
148+ {
149+ label : "Domain Expertise" ,
150+ score : report . domain_score ,
151+ confidence : report . domain_confidence ,
152+ } ,
153+ {
154+ label : "Career Progression" ,
155+ score : report . career_score ,
156+ confidence : report . career_confidence ,
157+ } ,
158+ {
159+ label : "Cultural Alignment" ,
160+ score : report . cultural_score ,
161+ confidence : report . cultural_confidence ,
162+ } ,
163+ ] . map ( ( { label, score, confidence } ) => (
164+ < div key = { label } className = "mb-2" >
165+ < div className = "flex items-center justify-between mb-1" >
166+ < span className = "font-semibold" style = { { fontSize : '11px' } } > { label } </ span >
167+ < span className = "text-gray-600" style = { { fontSize : '9px' } } >
168+ { score } /100 · { confidence } % confidence
169+ </ span >
170+ </ div >
171+ < ProgressBar value = { score } />
172+ </ div >
173+ ) ) }
174+ </ div >
175+ </ div >
176+
177+ { /* Concern Tags */ }
178+ < div className = "mb-3 no-break" >
179+ < h2 className = "font-semibold mb-2" style = { { fontSize : '16px' } } > Concern Tags</ h2 >
180+ < div className = "bg-gray-50 rounded-lg px-5 py-3" >
181+ < div className = "flex flex-wrap gap-2" >
182+ { report . concern_tags ?. length ? (
183+ report . concern_tags . map ( ( tag , i ) => (
184+ < span
185+ key = { i }
186+ className = "px-3 py-1 rounded-full font-medium border-2 border-amber-300 bg-amber-50"
187+ style = { { fontSize : '12px' } }
188+ >
189+ { tag }
190+ </ span >
191+ ) )
192+ ) : (
193+ < span className = "text-gray-500" style = { { fontSize : '12px' } } > None.</ span >
194+ ) }
195+ </ div >
196+ </ div >
197+ </ div >
198+
199+ { /* Pros */ }
200+ < div className = "mb-3 no-break" >
201+ < h2 className = "font-semibold mb-2" style = { { fontSize : '16px' } } > Pros</ h2 >
202+ < div className = "bg-gray-50 rounded-lg px-5 py-3" >
203+ < ul className = "space-y-1" >
204+ { prosList . length ? (
205+ prosList . map ( ( p , i ) => (
206+ < li key = { i } className = "flex items-start gap-3" >
207+ < Check className = "h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" />
208+ < span className = "leading-normal" style = { { fontSize : '12px' } } > { p } </ span >
209+ </ li >
210+ ) )
211+ ) : (
212+ < li className = "text-gray-500" style = { { fontSize : '12px' } } > No pros listed.</ li >
213+ ) }
214+ </ ul >
215+ </ div >
216+ </ div >
217+
218+ { /* Cons */ }
219+ < div className = "mb-3 no-break" >
220+ < h2 className = "font-semibold mb-2" style = { { fontSize : '16px' } } > Cons</ h2 >
221+ < div className = "bg-gray-50 rounded-lg px-5 py-3" >
222+ < ul className = "space-y-1" >
223+ { consList . length ? (
224+ consList . map ( ( c , i ) => (
225+ < li key = { i } className = "flex items-start gap-3" >
226+ < X className = "h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
227+ < span className = "leading-normal" style = { { fontSize : '12px' } } > { c } </ span >
228+ </ li >
229+ ) )
230+ ) : (
231+ < li className = "text-gray-500" style = { { fontSize : '12px' } } > No cons listed.</ li >
232+ ) }
233+ </ ul >
234+ </ div >
235+ </ div >
236+
237+ { /* Hiring Advice */ }
238+ < div className = "mb-3 no-break" >
239+ < h2 className = "font-semibold mb-2" style = { { fontSize : '16px' } } > Hiring Advice</ h2 >
240+ < div className = "bg-gray-50 rounded-lg px-5 py-3" >
241+ < ul className = "space-y-0.5" >
242+ { hiring_advices . length ? (
243+ hiring_advices . map ( ( c , i ) => (
244+ < li key = { i } className = "flex items-start gap-3" >
245+ < span className = "text-black font-bold mt-0.5" > •</ span >
246+ < span className = "leading-normal" style = { { fontSize : '12px' } } > { c } </ span >
247+ </ li >
248+ ) )
249+ ) : (
250+ < li className = "text-gray-500" style = { { fontSize : '12px' } } > No advice listed.</ li >
251+ ) }
252+ </ ul >
253+ </ div >
254+ </ div >
255+
256+ { /* Candidate Advice */ }
257+ < div className = "mb-3 no-break" >
258+ < h2 className = "font-semibold mb-2" style = { { fontSize : '16px' } } > Candidate Advice</ h2 >
259+ < div className = "bg-gray-50 rounded-lg px-5 py-3" >
260+ < ul className = "space-y-1" >
261+ { candidate_advices . length ? (
262+ candidate_advices . map ( ( c , i ) => (
263+ < li key = { i } className = "flex items-start gap-3" >
264+ < span className = "text-black font-bold mt-0.5" > •</ span >
265+ < span className = "leading-normal" style = { { fontSize : '12px' } } > { c } </ span >
266+ </ li >
267+ ) )
268+ ) : (
269+ < li className = "text-gray-500" style = { { fontSize : '12px' } } > No advice listed.</ li >
270+ ) }
271+ </ ul >
272+ </ div >
273+ </ div >
274+
275+ { /* Footer */ }
276+ < div className = "text-center mt-6 pt-3 border-t text-gray-500" >
277+ < p style = { { fontSize : '10px' } } > Generated by Bounteer Role Fit Index System</ p >
278+ </ div >
279+ </ div >
280+ ) ;
281+ }
282+ ) ;
283+
284+ PrintableReport . displayName = "PrintableReport" ;
285+
286+ export default PrintableReport ;
0 commit comments