Feature: Remove message, Upgrade: pull better job data, add dedicated status section to job applications
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
|
||||
import { api } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
@@ -231,6 +233,17 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const deleteMessage = async (messageId: number) => {
|
||||
try {
|
||||
await api.delete(`/correspondence/${messageId}`);
|
||||
await load();
|
||||
toast("Message removed.", "success");
|
||||
} catch {
|
||||
toast("Failed to remove message.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const importGmailMessage = async (messageId: string) => {
|
||||
try {
|
||||
setImportingMessageId(messageId);
|
||||
@@ -303,11 +316,16 @@ export default function Correspondence({ jobId }: { jobId: number }) {
|
||||
{m.content}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" sx={{ display: "block", mt: 0.75, color: "text.secondary" }}>
|
||||
{isMe ? "Me" : "Company"}
|
||||
{m.channel ? ` - ${m.channel}` : ""}
|
||||
{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "flex-end", mt: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||
{isMe ? "Me" : "Company"}
|
||||
{m.channel ? ` - ${m.channel}` : ""}
|
||||
{m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={() => void deleteMessage(m.id)} sx={{ color: "text.secondary" }}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
MenuItem,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
@@ -56,6 +58,8 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
|
||||
const [company, setCompany] = useState<Company | null>(null);
|
||||
const [jobTitle, setJobTitle] = useState("");
|
||||
const [status, setStatus] = useState("Applied");
|
||||
const [initialStatus, setInitialStatus] = useState("Applied");
|
||||
const [statusChangedAt, setStatusChangedAt] = useState(() => new Date().toISOString().slice(0, 10));
|
||||
const [dateApplied, setDateApplied] = useState(() => new Date().toISOString().slice(0, 10));
|
||||
const [location, setLocation] = useState("");
|
||||
const [salary, setSalary] = useState("");
|
||||
@@ -86,6 +90,8 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
|
||||
setCompany(j.company ?? null);
|
||||
setJobTitle(j.jobTitle ?? "");
|
||||
setStatus(j.status ?? "Applied");
|
||||
setInitialStatus(j.status ?? "Applied");
|
||||
setStatusChangedAt(new Date().toISOString().slice(0, 10));
|
||||
setDateApplied(toDateInputValue(j.dateApplied));
|
||||
setLocation(j.location ?? "");
|
||||
setSalary(j.salary ?? "");
|
||||
@@ -121,6 +127,7 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
|
||||
jobTitle: jobTitle.trim(),
|
||||
companyId: company.id,
|
||||
status,
|
||||
statusChangedAt: status !== initialStatus ? statusChangedAt || null : null,
|
||||
responseReceived,
|
||||
responseDate: responseReceived && responseDate ? responseDate : null,
|
||||
location: location.trim() || null,
|
||||
@@ -154,154 +161,107 @@ export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props)
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>Edit job</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: 2,
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
options={companies}
|
||||
getOptionLabel={(c) => c.name}
|
||||
value={company}
|
||||
onChange={(_, v) => setCompany(v)}
|
||||
renderInput={(params) => <TextField {...params} label="Company" />}
|
||||
/>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Application details</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, mt: 1 }}>
|
||||
<Autocomplete
|
||||
options={companies}
|
||||
getOptionLabel={(c) => c.name}
|
||||
value={company}
|
||||
onChange={(_, v) => setCompany(v)}
|
||||
renderInput={(params) => <TextField {...params} label="Company" />}
|
||||
/>
|
||||
<TextField label="Job title" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
<TextField
|
||||
label="Applied on"
|
||||
type="date"
|
||||
value={dateApplied}
|
||||
onChange={(e) => setDateApplied(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField label="Job URL" value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<TextField select label="Status" value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{s}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Status update</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 2, mt: 1 }}>
|
||||
<TextField select label="Current status" value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<MenuItem key={s} value={s}>{s}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Status changed on"
|
||||
type="date"
|
||||
value={statusChangedAt}
|
||||
onChange={(e) => setStatusChangedAt(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
helperText={status === initialStatus ? "Only used when you change the status." : "This date will be recorded in the timeline."}
|
||||
/>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={responseReceived} onChange={(e) => setResponseReceived(e.target.checked)} />}
|
||||
label="Reply received"
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Reply received on"
|
||||
type="date"
|
||||
disabled={!responseReceived}
|
||||
value={responseDate}
|
||||
onChange={(e) => setResponseDate(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField label="Next action" value={nextAction} onChange={(e) => setNextAction(e.target.value)} />
|
||||
<TextField
|
||||
label="Follow up on"
|
||||
type="date"
|
||||
value={followUpAt}
|
||||
onChange={(e) => setFollowUpAt(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<TextField label="Job Title" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Role details</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, mt: 1 }}>
|
||||
<TextField label="Location" value={location} onChange={(e) => setLocation(e.target.value)} />
|
||||
<TextField label="Salary" value={salary} onChange={(e) => setSalary(e.target.value)} />
|
||||
<TextField
|
||||
label="Deadline"
|
||||
type="date"
|
||||
value={deadline}
|
||||
onChange={(e) => setDeadline(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField label="Description language" value={descriptionLanguage} onChange={(e) => setDescriptionLanguage(e.target.value)} />
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<TagsInput value={tags} onChange={setTags} />
|
||||
</Box>
|
||||
<TextField label="Notes" value={notes} onChange={(e) => setNotes(e.target.value)} multiline rows={4} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Description (original)" value={description} onChange={(e) => setDescription(e.target.value)} multiline rows={6} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Translated description" value={translatedDescription} onChange={(e) => setTranslatedDescription(e.target.value)} multiline rows={6} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Cover letter" value={coverLetterText} onChange={(e) => setCoverLetterText(e.target.value)} multiline rows={6} sx={{ gridColumn: "1 / -1" }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<TextField
|
||||
label="Date Applied"
|
||||
type="date"
|
||||
value={dateApplied}
|
||||
onChange={(e) => setDateApplied(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<TextField label="Location" value={location} onChange={(e) => setLocation(e.target.value)} />
|
||||
|
||||
<TextField label="Salary" value={salary} onChange={(e) => setSalary(e.target.value)} />
|
||||
|
||||
<TextField label="Next Action" value={nextAction} onChange={(e) => setNextAction(e.target.value)} />
|
||||
|
||||
<TextField
|
||||
label="Follow Up"
|
||||
type="date"
|
||||
value={followUpAt}
|
||||
onChange={(e) => setFollowUpAt(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Job URL"
|
||||
value={jobUrl}
|
||||
onChange={(e) => setJobUrl(e.target.value)}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Notes"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
multiline
|
||||
rows={4}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description (Original)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
multiline
|
||||
rows={6}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Translated Description"
|
||||
value={translatedDescription}
|
||||
onChange={(e) => setTranslatedDescription(e.target.value)}
|
||||
multiline
|
||||
rows={6}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description Language"
|
||||
value={descriptionLanguage}
|
||||
onChange={(e) => setDescriptionLanguage(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Deadline"
|
||||
type="date"
|
||||
value={deadline}
|
||||
onChange={(e) => setDeadline(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<TagsInput value={tags} onChange={setTags} />
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
label="Cover Letter"
|
||||
value={coverLetterText}
|
||||
onChange={(e) => setCoverLetterText(e.target.value)}
|
||||
multiline
|
||||
rows={6}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={responseReceived} onChange={(e) => setResponseReceived(e.target.checked)} />}
|
||||
label="Response received"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Response Date"
|
||||
type="date"
|
||||
disabled={!responseReceived}
|
||||
value={responseDate}
|
||||
onChange={(e) => setResponseDate(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", gap: 2, flexWrap: "wrap" }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasResume} onChange={(e) => setHasResume(e.target.checked)} />}
|
||||
label="Resume"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasCoverLetter} onChange={(e) => setHasCoverLetter(e.target.checked)} />}
|
||||
label="Cover letter"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasPortfolio} onChange={(e) => setHasPortfolio(e.target.checked)} />}
|
||||
label="Portfolio"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={hasOtherAttachment} onChange={(e) => setHasOtherAttachment(e.target.checked)} />}
|
||||
label="Other attachment"
|
||||
/>
|
||||
</Box>
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary" }}>Attachments checklist</Typography>
|
||||
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap", mt: 1 }}>
|
||||
<FormControlLabel control={<Checkbox checked={hasResume} onChange={(e) => setHasResume(e.target.checked)} />} label="Resume" />
|
||||
<FormControlLabel control={<Checkbox checked={hasCoverLetter} onChange={(e) => setHasCoverLetter(e.target.checked)} />} label="Cover letter" />
|
||||
<FormControlLabel control={<Checkbox checked={hasPortfolio} onChange={(e) => setHasPortfolio(e.target.checked)} />} label="Portfolio" />
|
||||
<FormControlLabel control={<Checkbox checked={hasOtherAttachment} onChange={(e) => setHasOtherAttachment(e.target.checked)} />} label="Other attachment" />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button variant="contained" onClick={save} disabled={!canSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="contained" onClick={save} disabled={!canSave}>Save</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user