Skip to content

Commit e73f0c1

Browse files
committed
printable report
1 parent 29ff24b commit e73f0c1

File tree

5 files changed

+442
-100
lines changed

5 files changed

+442
-100
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [0.1.4] - 2025-09-18
4+
### Added
5+
- printable report
6+
37
## [0.1.3] - 2025-09-18
48
### Modified
59
- legal

package-lock.json

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"react": "^19.1.0",
3232
"react-dom": "^19.1.0",
3333
"react-hook-form": "^7.62.0",
34+
"react-to-print": "^3.1.1",
3435
"sharp": "^0.34.3",
3536
"tailwind-merge": "^3.3.1",
3637
"tailwindcss": "^4.1.3",
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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

Comments
 (0)