First Commit
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
MenuItem,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { Company, JobApplication } from "../types";
|
||||
import { useToast } from "../toast";
|
||||
import { useCompanies } from "../hooks/useCompanies";
|
||||
import TagsInput from "./TagsInput";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
jobId: number | null;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = ["Applied", "Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const;
|
||||
|
||||
function toDateInputValue(isoLike?: string): string {
|
||||
if (!isoLike) return new Date().toISOString().slice(0, 10);
|
||||
const d = new Date(isoLike);
|
||||
if (Number.isNaN(+d)) return new Date().toISOString().slice(0, 10);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function parseTags(raw: any): string[] {
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw.filter((x) => typeof x === "string");
|
||||
if (typeof raw !== "string") return [];
|
||||
try {
|
||||
const p = JSON.parse(raw);
|
||||
return Array.isArray(p) ? p.filter((x) => typeof x === "string") : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function EditJobDialog({ open, jobId, onClose, onSaved }: Props) {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { companies } = useCompanies();
|
||||
|
||||
const [company, setCompany] = useState<Company | null>(null);
|
||||
const [jobTitle, setJobTitle] = useState("");
|
||||
const [status, setStatus] = useState("Applied");
|
||||
const [dateApplied, setDateApplied] = useState(() => new Date().toISOString().slice(0, 10));
|
||||
const [location, setLocation] = useState("");
|
||||
const [salary, setSalary] = useState("");
|
||||
const [nextAction, setNextAction] = useState("");
|
||||
const [followUpAt, setFollowUpAt] = useState<string>("");
|
||||
const [jobUrl, setJobUrl] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [translatedDescription, setTranslatedDescription] = useState("");
|
||||
const [descriptionLanguage, setDescriptionLanguage] = useState("");
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [deadline, setDeadline] = useState<string>("");
|
||||
const [coverLetterText, setCoverLetterText] = useState("");
|
||||
const [responseReceived, setResponseReceived] = useState(false);
|
||||
const [responseDate, setResponseDate] = useState<string>("");
|
||||
const [hasResume, setHasResume] = useState(false);
|
||||
const [hasCoverLetter, setHasCoverLetter] = useState(false);
|
||||
const [hasPortfolio, setHasPortfolio] = useState(false);
|
||||
const [hasOtherAttachment, setHasOtherAttachment] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId) return;
|
||||
setLoading(true);
|
||||
api
|
||||
.get<JobApplication>(`/jobapplications/${jobId}`)
|
||||
.then((r) => {
|
||||
const j = r.data;
|
||||
setCompany(j.company ?? null);
|
||||
setJobTitle(j.jobTitle ?? "");
|
||||
setStatus(j.status ?? "Applied");
|
||||
setDateApplied(toDateInputValue(j.dateApplied));
|
||||
setLocation(j.location ?? "");
|
||||
setSalary(j.salary ?? "");
|
||||
setNextAction((j as any).nextAction ?? "");
|
||||
setFollowUpAt((j as any).followUpAt ? toDateInputValue((j as any).followUpAt) : "");
|
||||
setJobUrl(j.jobUrl ?? "");
|
||||
setNotes(j.notes ?? "");
|
||||
setDescription((j as any).description ?? "");
|
||||
setTranslatedDescription((j as any).translatedDescription ?? "");
|
||||
setDescriptionLanguage((j as any).descriptionLanguage ?? "");
|
||||
setTags(parseTags((j as any).tags));
|
||||
setDeadline((j as any).deadline ? toDateInputValue((j as any).deadline) : "");
|
||||
setCoverLetterText(j.coverLetterText ?? "");
|
||||
setResponseReceived(Boolean(j.responseReceived));
|
||||
setResponseDate(j.responseDate ? toDateInputValue(j.responseDate) : "");
|
||||
setHasResume(Boolean((j as any).hasResume));
|
||||
setHasCoverLetter(Boolean((j as any).hasCoverLetter));
|
||||
setHasPortfolio(Boolean((j as any).hasPortfolio));
|
||||
setHasOtherAttachment(Boolean((j as any).hasOtherAttachment));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [open, jobId]);
|
||||
|
||||
const canSave = useMemo(() => {
|
||||
return !!company?.id && jobTitle.trim().length > 0 && !loading;
|
||||
}, [company, jobTitle, loading]);
|
||||
|
||||
const save = async () => {
|
||||
if (!jobId || !company?.id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.put(`/jobapplications/${jobId}`, {
|
||||
jobTitle: jobTitle.trim(),
|
||||
companyId: company.id,
|
||||
status,
|
||||
responseReceived,
|
||||
responseDate: responseReceived && responseDate ? responseDate : null,
|
||||
location: location.trim() || null,
|
||||
salary: salary.trim() || null,
|
||||
nextAction: nextAction.trim() || null,
|
||||
followUpAt: followUpAt || null,
|
||||
hasResume,
|
||||
hasCoverLetter,
|
||||
hasPortfolio,
|
||||
hasOtherAttachment,
|
||||
notes: notes || null,
|
||||
description: description || null,
|
||||
translatedDescription: translatedDescription || null,
|
||||
descriptionLanguage: descriptionLanguage || null,
|
||||
tags: tags.length ? JSON.stringify(tags) : null,
|
||||
deadline: deadline || null,
|
||||
coverLetterText: coverLetterText || null,
|
||||
dateApplied: dateApplied || null,
|
||||
});
|
||||
toast("Saved.", "success");
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch {
|
||||
toast("Save failed.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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" />}
|
||||
/>
|
||||
|
||||
<TextField select label="Status" value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{s}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField label="Job Title" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
|
||||
<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>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button variant="contained" onClick={save} disabled={!canSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user