Dashboard upgrades, workflows added and assitant emailer
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Box, Chip, Dialog, DialogContent, DialogTitle, Tab, Tabs, Typography } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Tab,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { JobApplication } from "../types";
|
||||
@@ -9,6 +20,13 @@ import Correspondence from "./Correspondence";
|
||||
import Attachments from "./Attachments";
|
||||
import JobFlowBar from "./JobFlowBar";
|
||||
|
||||
type FollowUpDraft = {
|
||||
subject: string;
|
||||
body: string;
|
||||
reason: string;
|
||||
suggestedSendOn: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
jobId: number | null;
|
||||
@@ -33,14 +51,15 @@ function statusChipColor(status: string): "default" | "primary" | "warning" | "e
|
||||
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 [history, setHistory] = useState<{ id: number; type: string; oldValue?: string; newValue?: string; note?: string; at: string }[]>([]);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [followUpDraft, setFollowUpDraft] = useState<FollowUpDraft | null>(null);
|
||||
const [loadingDraft, setLoadingDraft] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId) return;
|
||||
setTab(0);
|
||||
setFollowUpDraft(null);
|
||||
api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => setJob(r.data));
|
||||
api
|
||||
.get(`/auth/me`)
|
||||
@@ -52,6 +71,16 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
.catch(() => setHistory([]));
|
||||
}, [open, jobId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId || tab !== 4 || followUpDraft) return;
|
||||
setLoadingDraft(true);
|
||||
api
|
||||
.get<FollowUpDraft>(`/jobapplications/${jobId}/followup-draft`)
|
||||
.then((r) => setFollowUpDraft(r.data))
|
||||
.catch(() => setFollowUpDraft(null))
|
||||
.finally(() => setLoadingDraft(false));
|
||||
}, [open, jobId, tab, followUpDraft]);
|
||||
|
||||
const tags: string[] = (() => {
|
||||
const raw = job?.tags;
|
||||
if (!raw) return [];
|
||||
@@ -64,23 +93,12 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
})();
|
||||
|
||||
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(", ") || "";
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
@@ -93,6 +111,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
<Tab label="Correspondence" />
|
||||
<Tab label="Attachments" />
|
||||
<Tab label="Cover Letter" />
|
||||
<Tab label="Follow-up draft" />
|
||||
{isAdmin ? <Tab label="History" /> : null}
|
||||
</Tabs>
|
||||
|
||||
@@ -129,19 +148,13 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
<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" />)
|
||||
)}
|
||||
{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>
|
||||
@@ -154,26 +167,22 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
)}
|
||||
</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>
|
||||
@@ -186,20 +195,53 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
{tab === 3 && (
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}>
|
||||
<button onClick={() => navigator.clipboard.writeText(job?.coverLetterText || "")}>Copy</button>
|
||||
<Button size="small" variant="outlined" onClick={() => navigator.clipboard.writeText(job?.coverLetterText || "")}>Copy</Button>
|
||||
</Box>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{job?.coverLetterText ?? ""}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{tab === 4 && (
|
||||
<Box>
|
||||
{loadingDraft ? (
|
||||
<Box sx={{ py: 4, display: "flex", justifyContent: "center" }}>
|
||||
<CircularProgress size={28} />
|
||||
</Box>
|
||||
) : followUpDraft ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="overline">Reason</Typography>
|
||||
<Typography>{followUpDraft.reason}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Suggested send date</Typography>
|
||||
<Typography>{new Date(followUpDraft.suggestedSendOn).toLocaleDateString()}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Subject</Typography>
|
||||
<Typography sx={{ fontWeight: 700 }}>{followUpDraft.subject}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="overline">Draft</Typography>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap" }}>{followUpDraft.body}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button variant="contained" onClick={() => navigator.clipboard.writeText(`${followUpDraft.subject}\n\n${followUpDraft.body}`)}>
|
||||
Copy draft
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography sx={{ color: "text.secondary" }}>No draft available.</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{tab === 4 && isAdmin && (
|
||||
{tab === 5 && 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} />
|
||||
))
|
||||
history.map((e) => <PaperRow key={e.id} type={e.type} oldValue={e.oldValue} newValue={e.newValue} at={e.at} note={e.note} />)
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
@@ -208,34 +250,14 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function PaperRow({
|
||||
type,
|
||||
oldValue,
|
||||
newValue,
|
||||
at,
|
||||
note,
|
||||
}: {
|
||||
type: string;
|
||||
oldValue?: string;
|
||||
newValue?: string;
|
||||
at: string;
|
||||
note?: string;
|
||||
}) {
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
<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 ?? ""})
|
||||
{" "}({oldValue ?? ""} {oldValue || newValue ? "->" : ""} {newValue ?? ""})
|
||||
</span>
|
||||
) : null}
|
||||
</Typography>
|
||||
|
||||
Reference in New Issue
Block a user