Dashboard upgrades, workflows added and assitant emailer

This commit is contained in:
cesnimda
2026-03-21 13:25:13 +01:00
parent 8cc4b0dfce
commit 51a539068f
9 changed files with 1358 additions and 1421 deletions
@@ -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>