import React, { useEffect, useMemo, useState } from "react"; import { Box, Card, CardContent, Chip, IconButton, Menu, MenuItem, Paper, Typography, } from "@mui/material"; import { alpha, useTheme } from "@mui/material/styles"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import { api } from "../api"; import { JobApplication } from "../types"; import { useI18n } from "../i18n/I18nProvider"; const STATUSES = ["Applied", "Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const; type Status = (typeof STATUSES)[number]; function normalizeStatus(status: string): Status | "Other" { if (status === "Interviewing") return "Interview"; if ((STATUSES as readonly string[]).includes(status)) return status as Status; return "Other"; } function toneColor(theme: any, status: Status | "Other"): string { if (status === "Rejected") return theme.palette.error.main; if (status === "Waiting" || status === "Ghosted") return theme.palette.warning.main; if (status === "Offer") return theme.palette.success.main; if (status === "Interview") return alpha(theme.palette.primary.main, 0.95); return theme.palette.primary.main; } export default function KanbanBoard() { const theme = useTheme(); const { t } = useI18n(); const [jobs, setJobs] = useState([]); const [dragJobId, setDragJobId] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null); const [menuJobId, setMenuJobId] = useState(null); useEffect(() => { api.get("/jobapplications/board").then((r) => setJobs(r.data)); }, []); const groups = useMemo(() => { const map = new Map(); STATUSES.forEach((s) => map.set(s, [])); map.set("Other", []); for (const j of jobs) { const key = normalizeStatus(j.status); map.get(key)!.push(j); } map.forEach((arr, k) => { arr.sort((a, b) => +new Date(b.dateApplied) - +new Date(a.dateApplied)); map.set(k, arr); }); return map; }, [jobs]); const onDropTo = async (status: Status) => { if (!dragJobId) return; setDragJobId(null); await api.patch(`/jobapplications/${dragJobId}/status`, { status }); setJobs((prev) => prev.map((j) => (j.id === dragJobId ? { ...j, status } : j))); }; const setStatus = async (id: number, status: Status) => { await api.patch(`/jobapplications/${id}/status`, { status }); setJobs((prev) => prev.map((j) => (j.id === id ? { ...j, status } : j))); }; const currentMenuStatus = menuJobId == null ? null : normalizeStatus(jobs.find((j) => j.id === menuJobId)?.status ?? ""); return ( Drag cards between columns to update status. {STATUSES.map((status) => { const c = toneColor(theme, status); const list = groups.get(status) ?? []; return ( e.preventDefault()} onDrop={() => void onDropTo(status)} sx={{ p: 1.5, borderRadius: 3, minHeight: 220, border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.25 : 0.18)}`, background: alpha(c, theme.palette.mode === "dark" ? 0.10 : 0.06), }} > {status} {list.map((j) => ( setDragJobId(j.id)} onDragEnd={() => setDragJobId(null)} sx={{ cursor: "grab", borderRadius: 3, border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.22 : 0.14)}`, background: theme.palette.mode === "dark" ? "rgba(15,23,42,0.82)" : "rgba(255,255,255,0.96)", backdropFilter: "blur(8px)", color: theme.palette.mode === "dark" ? "#e5eefc" : "#0f172a", }} > {j.company?.name ?? ""} { e.stopPropagation(); setMenuJobId(j.id); setMenuAnchor(e.currentTarget); }} > {j.jobTitle} {j.location ? : null} ))} {list.length === 0 && ( Drop here )} ); })} { setMenuAnchor(null); setMenuJobId(null); }}> {(["Applied", "Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const) .filter((s) => s !== currentMenuStatus) .map((s) => ( { if (menuJobId) void setStatus(menuJobId, s); setMenuAnchor(null); setMenuJobId(null); }}> {t("jobTableSetStatus", { status: s })} ))} ); }