initial commit (#158)

This commit is contained in:
Shaheer Sarfaraz 2026-02-12 19:59:25 +00:00 committed by GitHub
parent 687fd5e91f
commit d4d2c0c26b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 86 additions and 3 deletions

View File

@ -40,8 +40,17 @@ vi.mock("@/components/ui/chart", () => ({
}));
vi.mock("recharts", () => ({
BarChart: ({ children }: { children: React.ReactNode }) => (
<div data-testid="bar-chart">{children}</div>
BarChart: ({
children,
data,
}: {
children: React.ReactNode;
data?: unknown;
}) => (
<div data-testid="bar-chart">
{children}
<div data-testid="bar-chart-data">{JSON.stringify(data)}</div>
</div>
),
Bar: () => <div data-testid="bar">Bar</div>,
Cell: () => <div data-testid="cell">Cell</div>,
@ -327,6 +336,69 @@ describe("ConversionAnalytics - Edge Cases", () => {
// Job should count in all stages it reached
expect(screen.getByTestId("bar-chart")).toBeInTheDocument();
});
it("adds rejected funnel bar using rejected outcome/reason code only", () => {
const today = mockDate.toISOString();
const jobs = [
createJob("job-1", today, [
createEvent("closed", 1704844800000),
createStageEvent({
id: "event-rejected-1",
applicationId: "job-1",
toStage: "closed",
occurredAt: 1704844800001,
outcome: "rejected",
}),
]),
createJob("job-2", today, [
createStageEvent({
id: "event-rejected-2",
applicationId: "job-2",
toStage: "closed",
occurredAt: 1704844800002,
metadata: { reasonCode: "rejected" },
}),
]),
createJob("job-3", today, [
createStageEvent({
id: "event-withdrawn",
applicationId: "job-3",
toStage: "closed",
occurredAt: 1704844800003,
outcome: "withdrawn",
}),
]),
];
render(
<ConversionAnalytics
jobsWithEvents={jobs}
error={null}
daysToShow={7}
/>,
);
expect(
screen.getByText(
"Funnel: Applied → Screening → Interview → Offer → Rejected",
),
).toBeInTheDocument();
const chartDataRaw = screen.getByTestId("bar-chart-data").textContent;
expect(chartDataRaw).not.toBeNull();
const chartData = JSON.parse(chartDataRaw ?? "[]") as Array<{
name: string;
value: number;
}>;
const rejectedDataPoint = chartData.find(
(point) => point.name === "Rejected",
);
expect(rejectedDataPoint).toEqual(
expect.objectContaining({ name: "Rejected", value: 2 }),
);
});
});
describe("Date Range and Invalid Dates", () => {

View File

@ -62,6 +62,7 @@ const FUNNEL_STAGES = [
{ key: "screening", label: "Screening", color: "#8b5cf6" },
{ key: "interview", label: "Interview", color: "#f59e0b" },
{ key: "offer", label: "Offer", color: "#10b981" },
{ key: "rejected", label: "Rejected", color: "#ef4444" },
] as const;
// Stages that count as "screening"
@ -87,6 +88,9 @@ const CONVERSION_STAGES = new Set([
// Stages that count as "offer"
const OFFER_STAGES = new Set(["offer"]);
const isRejectedEvent = (event: StageEvent) =>
event.outcome === "rejected" || event.metadata?.reasonCode === "rejected";
const toDateKey = (value: Date) => {
const year = value.getFullYear();
const month = `${value.getMonth() + 1}`.padStart(2, "0");
@ -100,6 +104,7 @@ const buildFunnelData = (jobsWithEvents: JobWithEvents[]): FunnelStage[] => {
let screening = 0;
let interview = 0;
let offer = 0;
let rejected = 0;
for (const job of jobsWithEvents) {
if (!job.appliedAt) continue;
@ -133,6 +138,11 @@ const buildFunnelData = (jobsWithEvents: JobWithEvents[]): FunnelStage[] => {
break;
}
}
const reachedRejected = job.events.some(isRejectedEvent);
if (reachedRejected) {
rejected++;
}
}
return [
@ -140,6 +150,7 @@ const buildFunnelData = (jobsWithEvents: JobWithEvents[]): FunnelStage[] => {
{ name: "Screening", value: screening, fill: FUNNEL_STAGES[1].color },
{ name: "Interview", value: interview, fill: FUNNEL_STAGES[2].color },
{ name: "Offer", value: offer, fill: FUNNEL_STAGES[3].color },
{ name: "Rejected", value: rejected, fill: FUNNEL_STAGES[4].color },
];
};
@ -312,7 +323,7 @@ export function ConversionAnalytics({
{/* Funnel Chart */}
<div>
<h4 className="mb-3 text-sm font-medium text-muted-foreground">
Funnel: Applied Screening Interview Offer
Funnel: Applied Screening Interview Offer Rejected
</h4>
<ChartContainer
config={chartConfig}