2025-06-18 14:26:07 -07:00

569 lines
17 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import {
Box,
Link,
Typography,
Avatar,
Grid,
SxProps,
CardActions,
Chip,
Stack,
CardHeader,
Button,
LinearProgress,
IconButton,
Tooltip,
Card,
CardContent,
Divider,
useTheme,
useMediaQuery,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Tabs,
Tab,
} from "@mui/material";
import PrintIcon from "@mui/icons-material/Print";
import {
Delete as DeleteIcon,
Restore as RestoreIcon,
Save as SaveIcon,
Edit as EditIcon,
Description as DescriptionIcon,
Work as WorkIcon,
Person as PersonIcon,
Schedule as ScheduleIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
} from "@mui/icons-material";
import PreviewIcon from "@mui/icons-material/Preview";
import EditDocumentIcon from "@mui/icons-material/EditDocument";
import { useReactToPrint } from "react-to-print";
import { useAuth } from "hooks/AuthContext";
import { useAppState } from "hooks/GlobalContext";
import { StyledMarkdown } from "components/StyledMarkdown";
import { Resume } from "types/types";
import { BackstoryTextField } from "components/BackstoryTextField";
import { JobInfo } from "./JobInfo";
interface ResumeInfoProps {
resume: Resume;
sx?: SxProps;
action?: string;
elevation?: number;
variant?: "minimal" | "small" | "normal" | "all" | null;
}
const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
const { setSnack } = useAppState();
const { resume } = props;
const { user, apiClient } = useAuth();
const { sx, action = "", elevation = 1, variant = "normal" } = props;
const theme = useTheme();
const isMobile =
useMediaQuery(theme.breakpoints.down("md")) || variant === "minimal";
const isAdmin = user?.isAdmin;
const [activeResume, setActiveResume] = useState<Resume>({ ...resume });
const [isContentExpanded, setIsContentExpanded] = useState(false);
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
const [deleted, setDeleted] = useState<boolean>(false);
const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
const [printDialogOpen, setPrintDialogOpen] = useState<boolean>(false);
const [editContent, setEditContent] = useState<string>("");
const [saving, setSaving] = useState<boolean>(false);
const contentRef = useRef<HTMLDivElement>(null);
const [tabValue, setTabValue] = useState("markdown");
const printContentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({
contentRef: printContentRef,
pageStyle: "@page { margin: 10px; }",
});
useEffect(() => {
if (resume && resume.id !== activeResume?.id) {
setActiveResume(resume);
}
}, [resume, activeResume]);
// Check if content needs truncation
useEffect(() => {
if (contentRef.current && resume.resume) {
const element = contentRef.current;
setShouldShowMoreButton(element.scrollHeight > element.clientHeight);
}
}, [resume.resume]);
const deleteResume = async (id: string | undefined) => {
if (id) {
try {
await apiClient.deleteResume(id);
setDeleted(true);
setSnack("Resume deleted successfully.");
} catch (error) {
setSnack("Failed to delete resume.");
}
}
};
const handleReset = async () => {
setActiveResume({ ...resume });
};
const handleSave = async () => {
setSaving(true);
try {
const result = await apiClient.updateResume(
activeResume.id || "",
editContent
);
const updatedResume = {
...activeResume,
resume: editContent,
updatedAt: new Date(),
};
setActiveResume(updatedResume);
setSnack("Resume updated successfully.");
} catch (error) {
setSnack("Failed to update resume.");
} finally {
setSaving(false);
}
};
const handleEditOpen = () => {
setEditContent(activeResume.resume);
setEditDialogOpen(true);
};
if (!resume) {
return <Box>No resume provided.</Box>;
}
const formatDate = (date: Date | undefined) => {
if (!date) return "N/A";
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
};
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
if (newValue === "print") {
reactToPrintFn();
return;
}
setTabValue(newValue);
};
return (
<Box
sx={{
display: "flex",
borderColor: "transparent",
borderWidth: 2,
borderStyle: "solid",
transition: "all 0.3s ease",
flexDirection: "column",
minWidth: 0,
opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted
? theme.palette.action.disabledBackground
: theme.palette.background.paper,
pointerEvents: deleted ? "none" : "auto",
...sx,
}}
>
<Box
sx={{
display: "flex",
flexGrow: 1,
p: 1,
pb: 0,
height: "100%",
flexDirection: "column",
alignItems: "stretch",
position: "relative",
}}
>
{/* Header Information */}
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
gap: 2,
mb: 2,
}}
>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Stack spacing={1}>
{activeResume.candidate && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<PersonIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold">
Candidate
</Typography>
</Box>
)}
<Typography variant="body2" color="text.secondary">
{activeResume.candidate?.fullName || activeResume.candidateId}
</Typography>
{activeResume.job && (
<>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
mt: 1,
}}
>
<WorkIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold">
Job
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
{activeResume.job.title} at {activeResume.job.company}
</Typography>
</>
)}
</Stack>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Stack spacing={1}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<ScheduleIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold">
Timeline
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
Created: {formatDate(activeResume.createdAt)}
</Typography>
<Typography variant="caption" color="text.secondary">
Updated: {formatDate(activeResume.updatedAt)}
</Typography>
<Typography variant="caption" color="text.secondary">
Resume ID: {activeResume.id}
</Typography>
</Stack>
</Grid>
</Grid>
</Box>
<Divider sx={{ mb: 2 }} />
{/* Resume Content */}
{activeResume.resume && (
<Card
elevation={0}
sx={{ m: 0, p: 0, background: "transparent !important" }}
>
<CardHeader
title="Resume Content"
avatar={<DescriptionIcon color="success" />}
sx={{ p: 0, pb: 1 }}
action={
isAdmin && (
<Tooltip title="Edit Resume Content">
<IconButton size="small" onClick={handleEditOpen}>
<EditIcon />
</IconButton>
</Tooltip>
)
}
/>
<CardContent sx={{ p: 0 }}>
<Box sx={{ position: "relative" }}>
<Typography
ref={contentRef}
variant="body2"
component="div"
sx={{
display: "-webkit-box",
WebkitLineClamp: isContentExpanded
? "unset"
: variant === "small"
? 5
: variant === "minimal"
? 3
: 10,
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
lineHeight: 1.6,
fontSize: "0.875rem !important",
whiteSpace: "pre-wrap",
fontFamily: "monospace",
backgroundColor: theme.palette.action.hover,
p: 2,
borderRadius: 1,
border: `1px solid ${theme.palette.divider}`,
}}
>
{activeResume.resume}
</Typography>
{shouldShowMoreButton && variant !== "all" && (
<Box
sx={{ display: "flex", justifyContent: "center", mt: 1 }}
>
<Button
variant="text"
size="small"
onClick={() => setIsContentExpanded(!isContentExpanded)}
startIcon={
isContentExpanded ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)
}
sx={{ fontSize: "0.75rem" }}
>
{isContentExpanded ? "Show Less" : "Show More"}
</Button>
</Box>
)}
</Box>
</CardContent>
</Card>
)}
{variant === "all" && activeResume.resume && (
<Box sx={{ mt: 2 }}>
<StyledMarkdown content={activeResume.resume} />
</Box>
)}
</Box>
{/* Admin Controls */}
{isAdmin && (
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}>
<Box
sx={{
display: "flex",
flexDirection: "row",
pl: 1,
pr: 1,
gap: 1,
alignContent: "center",
height: "32px",
}}
>
<Tooltip title="Edit Resume">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleEditOpen();
}}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete Resume">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
deleteResume(activeResume.id);
}}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title="Reset Resume">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleReset();
}}
>
<RestoreIcon />
</IconButton>
</Tooltip>
</Box>
{saving && (
<Box sx={{ mt: 1 }}>
<LinearProgress />
<Typography variant="caption" sx={{ mt: 0.5 }}>
Saving resume...
</Typography>
</Box>
)}
</Box>
)}
{/* Print Dialog */}
<Dialog
open={printDialogOpen}
onClose={() => {}} //setPrintDialogOpen(false)}
maxWidth="lg"
fullWidth
fullScreen={true}
>
<StyledMarkdown
content={activeResume.resume}
sx={{
p: 2,
position: "relative",
maxHeight: "100%",
width: "100%",
display: "flex",
flexGrow: 1,
flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */,
}}
/>
</Dialog>
{/* Edit Dialog */}
<Dialog
open={editDialogOpen}
onClose={() => setEditDialogOpen(false)}
maxWidth="lg"
fullWidth
disableEscapeKeyDown={true}
fullScreen={true}
>
<DialogTitle>
Edit Resume Content
<Typography variant="caption" display="block" color="text.secondary">
Resume for{" "}
{activeResume.candidate?.fullName || activeResume.candidateId},{" "}
{activeResume.job?.title || "No Job Title Assigned"},{" "}
{activeResume.job?.company || "No Company Assigned"}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Resume ID: # {activeResume.id}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Last saved:{" "}
{activeResume.updatedAt
? new Date(activeResume.updatedAt).toLocaleString()
: "N/A"}
</Typography>
</DialogTitle>
<DialogContent
sx={{
position: "relative",
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab
value="markdown"
icon={<EditDocumentIcon />}
label="Markdown"
/>
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
<Tab value="job" icon={<WorkIcon />} label="Job" />
<Tab value="print" icon={<PrintIcon />} label="Print" />
</Tabs>
<Box
ref={printContentRef}
sx={{
display: "flex",
flexDirection: "column",
height: "100%" /* Restrict to main-container's height */,
width: "100%",
minHeight: 0 /* Prevent flex overflow */,
//maxHeight: "min-content",
"& > *:not(.Scrollable)": {
flexShrink: 0 /* Prevent shrinking */,
},
position: "relative",
}}
>
{tabValue === "markdown" && (
<BackstoryTextField
value={editContent}
onChange={(value) => setEditContent(value)}
style={{
position: "relative",
// maxHeight: "100%",
height: "100%",
width: "100%",
display: "flex",
minHeight: "100%",
flexGrow: 1,
flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */,
}}
placeholder="Enter resume content..."
/>
)}
{tabValue === "preview" && (
<>
<StyledMarkdown
sx={{
p: 2,
position: "relative",
maxHeight: "100%",
width: "100%",
display: "flex",
flexGrow: 1,
flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */,
}}
content={editContent}
/>
<Box sx={{ pb: 2 }}></Box>
</>
)}
{tabValue === "job" && activeResume.job && (
<JobInfo
variant="all"
job={activeResume.job}
sx={{
p: 2,
position: "relative",
maxHeight: "100%",
width: "100%",
display: "flex",
flexGrow: 1,
flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */,
}}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleSave}
variant="contained"
disabled={saving}
startIcon={<SaveIcon />}
>
{saving ? "Saving..." : "Save"}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export { ResumeInfo };