import React, { useMemo } from "react"; import { Box, Chip, Typography, useTheme } from "@mui/material"; import { alpha } from "@mui/material/styles"; import WorkOutlineIcon from "@mui/icons-material/WorkOutline"; import MarkEmailReadIcon from "@mui/icons-material/MarkEmailRead"; import ForumIcon from "@mui/icons-material/Forum"; import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; import CloseIcon from "@mui/icons-material/Close"; import { JobApplication } from "../types"; type HistoryEvent = { id: number; type: string; oldValue?: string; newValue?: string; note?: string; at: string; }; type FlowItem = { key: string; label: string; at: Date; color: string; icon: React.ReactNode; }; function normalizeStatus(status?: string): string { if (!status) return "Applied"; if (status === "Interviewing") return "Interview"; return status; } function parseResponseDate(value?: string): Date | null { if (!value) return null; const parts = value.split(":"); const raw = parts.length > 1 ? parts.slice(1).join(":") : ""; const d = new Date(raw); return Number.isNaN(+d) ? null : d; } function firstStatusChange(history: HistoryEvent[], status: string): Date | null { const match = history.find((item) => item.type === "StatusChanged" && normalizeStatus(item.newValue) === status); if (!match) return null; const d = new Date(match.at); return Number.isNaN(+d) ? null : d; } function firstResponse(history: HistoryEvent[], job: JobApplication): Date | null { if (job.responseDate) { const d = new Date(job.responseDate); if (!Number.isNaN(+d)) return d; } const explicit = history.find((item) => item.type === "ReplyReceived"); if (explicit) { const d = new Date(explicit.at); if (!Number.isNaN(+d)) return d; } const updated = history.find((item) => item.type === "ResponseUpdated"); if (!updated) return null; return parseResponseDate(updated.newValue) ?? new Date(updated.at); } export default function JobFlowBar({ job, history = [] }: { job: JobApplication | null; history?: HistoryEvent[] }) { const theme = useTheme(); const items = useMemo(() => { if (!job) return [] as FlowItem[]; const appliedAt = new Date(job.dateApplied); const replyAt = firstResponse(history, job); const interviewAt = firstStatusChange(history, "Interview") || (normalizeStatus(job.status) === "Interview" ? replyAt ?? appliedAt : null); const offerAt = firstStatusChange(history, "Offer") || (normalizeStatus(job.status) === "Offer" ? replyAt ?? interviewAt ?? appliedAt : null); const rejectedLabel = normalizeStatus(job.status) === "Ghosted" ? "Ghosted" : "Rejected"; const rejectedAt = firstStatusChange(history, "Rejected") || firstStatusChange(history, "Ghosted") || ((normalizeStatus(job.status) === "Rejected" || normalizeStatus(job.status) === "Ghosted") ? replyAt ?? appliedAt : null); const next: FlowItem[] = [ { key: "applied", label: "Applied", at: appliedAt, color: theme.palette.info.main, icon: , }, ]; if (replyAt) { next.push({ key: "reply", label: "Reply", at: replyAt, color: theme.palette.success.main, icon: , }); } if (interviewAt) { next.push({ key: "interview", label: "Interview", at: interviewAt, color: theme.palette.primary.main, icon: , }); } if (offerAt) { next.push({ key: "offer", label: "Offer", at: offerAt, color: theme.palette.success.dark, icon: , }); } else if (rejectedAt) { next.push({ key: normalizeStatus(job.status).toLowerCase(), label: rejectedLabel, at: rejectedAt, color: theme.palette.error.main, icon: , }); } return next .filter((item) => !Number.isNaN(+item.at)) .sort((a, b) => +a.at - +b.at) .filter((item, index, arr) => index === arr.findIndex((other) => other.key === item.key)); }, [history, job, theme.palette.error.main, theme.palette.info.main, theme.palette.primary.main, theme.palette.success.dark, theme.palette.success.main]); if (!job || items.length === 0) return null; return ( Flow {items.map((item, index) => ( {index < items.length - 1 ? ( {"->"} ) : null} ))} ); }