First Commit
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Box, Chip, Dialog, DialogContent, DialogTitle, Tab, Tabs, Typography } from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { JobApplication } from "../types";
|
||||
|
||||
import Correspondence from "./Correspondence";
|
||||
import Attachments from "./Attachments";
|
||||
import JobFlowBar from "./JobFlowBar";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
jobId: number | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function statusChipColor(status: string): "default" | "primary" | "warning" | "error" | "success" {
|
||||
switch (status) {
|
||||
case "Rejected":
|
||||
return "error";
|
||||
case "Waiting":
|
||||
case "Ghosted":
|
||||
return "warning";
|
||||
case "Offer":
|
||||
return "success";
|
||||
case "Applied":
|
||||
default:
|
||||
return "primary";
|
||||
}
|
||||
}
|
||||
|
||||
export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
const [job, setJob] = useState<JobApplication | null>(null);
|
||||
const [tab, setTab] = useState(0);
|
||||
const [history, setHistory] = useState<
|
||||
{ id: number; type: string; oldValue?: string; newValue?: string; note?: string; at: string }[]
|
||||
>([]);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId) return;
|
||||
setTab(0);
|
||||
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => setJob(r.data));
|
||||
api
|
||||
.get(`/auth/me`)
|
||||
.then((r) => setIsAdmin(Boolean(r.data?.roles?.includes("Admin"))))
|
||||
.catch(() => setIsAdmin(false));
|
||||
api
|
||||
.get(`/jobapplications/${jobId}/history`)
|
||||
.then((r) => setHistory(r.data))
|
||||
.catch(() => setHistory([]));
|
||||
}, [open, jobId]);
|
||||
|
||||
const tags: string[] = (() => {
|
||||
const raw = job?.tags;
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
const title = job ? `${job.company?.name ?? ""} - ${job.jobTitle}` : "Job Application";
|
||||
const checklist =
|
||||
[job?.hasResume ? "Resume" : null, job?.hasCoverLetter ? "Cover letter" : null, job?.hasPortfolio ? "Portfolio" : null, job?.hasOtherAttachment ? "Other" : null]
|
||||
.filter(Boolean)
|
||||
.join(", ") || "";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 2,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
{job && <Chip label={job.status} color={statusChipColor(job.status)} size="small" />}
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<JobFlowBar job={job} />
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Correspondence" />
|
||||
<Tab label="Attachments" />
|
||||
<Tab label="Cover Letter" />
|
||||
{isAdmin ? <Tab label="History" /> : null}
|
||||
</Tabs>
|
||||
|
||||
{tab === 0 && (
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="overline">Date Applied</Typography>
|
||||
<Typography>{job ? new Date(job.dateApplied).toLocaleDateString() : ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Days Since</Typography>
|
||||
<Typography>{job?.daysSince ?? ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Location</Typography>
|
||||
<Typography>{job?.location ?? ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Salary</Typography>
|
||||
<Typography>{job?.salary ?? ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Next Action</Typography>
|
||||
<Typography>{job?.nextAction ?? ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Follow Up</Typography>
|
||||
<Typography>{job?.followUpAt ? new Date(job.followUpAt).toLocaleDateString() : ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Deadline</Typography>
|
||||
<Typography>{job?.deadline ? new Date(job.deadline).toLocaleDateString() : ""}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Tags</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 0.5 }}>
|
||||
{tags.length === 0 ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>-</Typography>
|
||||
) : (
|
||||
tags.map((t) => <Chip key={t} label={t} size="small" />)
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Attachment Types</Typography>
|
||||
<Typography>{checklist}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Job URL</Typography>
|
||||
<Typography>
|
||||
{job?.jobUrl ? (
|
||||
<a href={job.jobUrl} target="_blank" rel="noreferrer">
|
||||
{job.jobUrl}
|
||||
</a>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Description (original)</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.description ?? ""}</Typography>
|
||||
</Box>
|
||||
|
||||
{job?.translatedDescription ? (
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Translated description</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{job.translatedDescription}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{job?.fullSummary || job?.shortSummary ? (
|
||||
<Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
||||
<Typography variant="overline">Summary</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.fullSummary ?? job?.shortSummary}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline">Notes</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.notes ?? ""}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 1 && jobId && <Correspondence jobId={jobId} />}
|
||||
{tab === 2 && jobId && <Attachments jobId={jobId} />}
|
||||
{tab === 3 && (
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}>
|
||||
<button onClick={() => navigator.clipboard.writeText(job?.coverLetterText || "")}>Copy</button>
|
||||
</Box>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.coverLetterText ?? ""}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 4 && isAdmin && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{history.length === 0 ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>No history yet.</Typography>
|
||||
) : (
|
||||
history.map((e) => (
|
||||
<PaperRow key={e.id} type={e.type} oldValue={e.oldValue} newValue={e.newValue} at={e.at} note={e.note} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function PaperRow({
|
||||
type,
|
||||
oldValue,
|
||||
newValue,
|
||||
at,
|
||||
note,
|
||||
}: {
|
||||
type: string;
|
||||
oldValue?: string;
|
||||
newValue?: string;
|
||||
at: string;
|
||||
note?: string;
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
border: "1px solid rgba(15,23,42,0.08)",
|
||||
borderRadius: 2,
|
||||
p: 1.25,
|
||||
background: "rgba(255,255,255,0.6)",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontWeight: 900, lineHeight: 1.25 }}>
|
||||
{type}
|
||||
{oldValue || newValue ? (
|
||||
<span style={{ fontWeight: 700, opacity: 0.7 }}>
|
||||
{" "}
|
||||
({oldValue ?? ""} {oldValue || newValue ? "->" : ""} {newValue ?? ""})
|
||||
</span>
|
||||
) : null}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||
{at ? new Date(at).toLocaleString() : ""}
|
||||
{note ? ` - ${note}` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user