import React, { useMemo, useState } from "react"; import { Box, Card, CardContent, 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 ViewStateNotice from "./ViewStateNotice"; import { JobApplication } from "../types"; import { useI18n } from "../i18n/I18nProvider"; import { useViewResource } from "../hooks/useViewResource"; import { PIPELINE_STATUSES, PipelineStatus, normalizeStatus, statusLabel, statusTone } from "../pipeline"; const STATUSES = PIPELINE_STATUSES; type Status = PipelineStatus; const TONE_PALETTE: Record string> = { error: (theme) => theme.palette.error.main, warning: (theme) => theme.palette.warning.main, success: (theme) => theme.palette.success.main, info: (theme) => alpha(theme.palette.primary.main, 0.95), primary: (theme) => theme.palette.primary.main, default: (theme) => theme.palette.primary.main, }; function toneColor(theme: any, status: Status | "Other"): string { return TONE_PALETTE[statusTone(status)](theme); } export default function KanbanBoard() { const theme = useTheme(); const { t } = useI18n(); const [dragJobId, setDragJobId] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null); const [menuJobId, setMenuJobId] = useState(null); const jobsResource = useViewResource( async () => { const response = await api.get("/jobapplications/board"); return response.data; }, { initialData: [], errorMessage: "Unable to load the board right now.", deps: [], }, ); const jobs = jobsResource.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 }); jobsResource.setData((prev) => prev.map((j) => (j.id === dragJobId ? { ...j, status } : j))); }; const setStatus = async (id: number, status: Status) => { await api.patch(`/jobapplications/${id}/status`, { status }); jobsResource.setData((prev) => prev.map((j) => (j.id === id ? { ...j, status } : j))); }; const currentMenuStatus = menuJobId == null ? null : normalizeStatus(jobs.find((j) => j.id === menuJobId)?.status ?? ""); return ( {t("kanbanHint")} {!jobsResource.loading && !jobsResource.error ? ( {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", borderColor: "divider", background: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.02) : alpha(theme.palette.text.primary, 0.015), }} > {statusLabel(t, status)} {list.length} {list.map((j) => ( setDragJobId(j.id)} onDragEnd={() => setDragJobId(null)} sx={{ cursor: "grab", borderRadius: 2.5, borderLeft: `4px solid ${c}`, boxShadow: theme.palette.mode === "dark" ? "none" : "0 1px 3px rgba(15,23,42,0.06)", }} > {j.jobTitle} { e.stopPropagation(); setMenuJobId(j.id); setMenuAnchor(e.currentTarget); }} > {[j.company?.name, j.location].filter(Boolean).join(" ยท ")} {j.daysSince}d ))} {list.length === 0 && ( {t("kanbanDropHere")} )} ); })} ) : null} { 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: statusLabel(t, s) })} ))} ); }