initial commit (#158)
This commit is contained in:
parent
687fd5e91f
commit
d4d2c0c26b
@ -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", () => {
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user