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
+99 -100
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from "react";
import {
Alert,
Autocomplete,
Box,
Button,
@@ -10,6 +11,9 @@ import {
DialogTitle,
Divider,
FormControlLabel,
List,
ListItem,
ListItemText,
MenuItem,
TextField,
Typography,
@@ -28,6 +32,21 @@ interface Props {
onCreated: () => void;
}
type DuplicateCandidate = {
id: number;
jobTitle: string;
company: string;
jobUrl?: string | null;
status: string;
dateApplied: string;
reason: string;
};
type DuplicateCheckResult = {
hasDuplicates: boolean;
matches: DuplicateCandidate[];
};
const STATUS_OPTIONS = ["Applied", "Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const;
function getTodayIso() {
@@ -40,6 +59,8 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
const [saving, setSaving] = useState(false);
const [importing, setImporting] = useState(false);
const [saveAndAddAnother, setSaveAndAddAnother] = useState(false);
const [duplicateCheck, setDuplicateCheck] = useState<DuplicateCheckResult | null>(null);
const { companies: cachedCompanies } = useCompanies();
const [companies, setCompanies] = useState<Company[]>([]);
@@ -101,6 +122,7 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
setHasCoverLetter(false);
setHasPortfolio(false);
setHasOtherAttachment(false);
setDuplicateCheck(null);
};
const normalizedCompanyName = companyInput.trim();
@@ -109,8 +131,39 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
return companies.find((c) => c.name.toLowerCase() === normalizedCompanyName.toLowerCase()) ?? null;
}, [companies, normalizedCompanyName]);
const selectedCompanyId = company?.id ?? matchingCompany?.id ?? 0;
const showNewCompanyFields = !company && !!normalizedCompanyName && !matchingCompany;
useEffect(() => {
if (!open) return;
const title = jobTitle.trim();
const url = jobUrl.trim();
if (!selectedCompanyId && !url) {
setDuplicateCheck(null);
return;
}
if (!title && !url) {
setDuplicateCheck(null);
return;
}
const timeout = window.setTimeout(() => {
api
.get<DuplicateCheckResult>("/jobapplications/duplicate-check", {
params: {
companyId: selectedCompanyId || undefined,
jobTitle: title || undefined,
jobUrl: url || undefined,
},
})
.then((r) => setDuplicateCheck(r.data))
.catch(() => setDuplicateCheck(null));
}, 350);
return () => window.clearTimeout(timeout);
}, [open, selectedCompanyId, jobTitle, jobUrl]);
const createCompany = async (): Promise<Company | null> => {
if (!normalizedCompanyName) return null;
@@ -209,10 +262,12 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
hasOtherAttachment,
});
resetForm();
onCreated();
onClose();
toast("Job added.", "success");
resetForm();
if (!saveAndAddAnother) {
onClose();
}
} catch {
toast("Failed to add job.", "error");
} finally {
@@ -258,16 +313,8 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
{showNewCompanyFields ? (
<Box sx={{ mt: 1, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
<TextField
label="Company location"
value={newCompanyLocation}
onChange={(e) => setNewCompanyLocation(e.target.value)}
/>
<TextField
label="Company source"
value={newCompanySource}
onChange={(e) => setNewCompanySource(e.target.value)}
/>
<TextField label="Company location" value={newCompanyLocation} onChange={(e) => setNewCompanyLocation(e.target.value)} />
<TextField label="Company source" value={newCompanySource} onChange={(e) => setNewCompanySource(e.target.value)} />
<Box sx={{ gridColumn: "1 / -1" }}>
<Button variant="outlined" onClick={() => void createCompany()}>
Create "{normalizedCompanyName}"
@@ -276,6 +323,22 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
</Box>
) : null}
{duplicateCheck?.hasDuplicates ? (
<Alert severity="warning" sx={{ mt: 2 }}>
<Typography sx={{ fontWeight: 800, mb: 0.75 }}>Possible duplicates found</Typography>
<List dense sx={{ py: 0 }}>
{duplicateCheck.matches.map((match) => (
<ListItem key={match.id} sx={{ px: 0 }}>
<ListItemText
primary={`${match.company} - ${match.jobTitle}`}
secondary={`${match.reason}${match.status}${new Date(match.dateApplied).toLocaleDateString()}`}
/>
</ListItem>
))}
</List>
</Alert>
) : null}
<Divider sx={{ my: 2 }} />
<Typography variant="overline" sx={{ display: "block" }}>
@@ -283,25 +346,14 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, mt: 1 }}>
<TextField
label="Job URL"
value={jobUrl}
onChange={(e) => setJobUrl(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField label="Job URL" value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
<Button onClick={() => void importFromUrl()} disabled={importing || !jobUrl.trim()}>
{importing ? "Importing..." : "Import from URL"}
</Button>
</Box>
<TextField
label="Date applied"
type="date"
value={dateApplied}
onChange={(e) => setDateApplied(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<TextField label="Date applied" type="date" value={dateApplied} onChange={(e) => setDateApplied(e.target.value)} InputLabelProps={{ shrink: true }} />
<TextField select label="Status" value={status} onChange={(e) => setStatus(e.target.value as any)}>
{STATUS_OPTIONS.map((s) => (
@@ -317,94 +369,41 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
<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="Follow up" type="date" value={followUpAt} onChange={(e) => setFollowUpAt(e.target.value)} InputLabelProps={{ shrink: true }} />
<TextField
label="Deadline"
type="date"
value={deadline}
onChange={(e) => setDeadline(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<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="Description (original)"
multiline
rows={6}
value={description}
onChange={(e) => setDescription(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
label="Translated description"
multiline
rows={6}
value={translatedDescription}
onChange={(e) => setTranslatedDescription(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
label="Description language (optional)"
value={descriptionLanguage}
onChange={(e) => setDescriptionLanguage(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
label="Notes"
multiline
rows={3}
value={notes}
onChange={(e) => setNotes(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
label="Cover letter"
multiline
rows={6}
value={coverLetter}
onChange={(e) => setCoverLetter(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField label="Description (original)" multiline rows={6} value={description} onChange={(e) => setDescription(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
<TextField label="Translated description" multiline rows={6} value={translatedDescription} onChange={(e) => setTranslatedDescription(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
<TextField label="Description language (optional)" value={descriptionLanguage} onChange={(e) => setDescriptionLanguage(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
<TextField label="Notes" multiline rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
<TextField label="Cover letter" multiline rows={6} value={coverLetter} onChange={(e) => setCoverLetter(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
<Box sx={{ gridColumn: "1 / -1" }}>
<Typography variant="overline" sx={{ display: "block", mt: 1 }}>
Attachments checklist
</Typography>
<Typography variant="overline" sx={{ display: "block", mt: 1 }}>Attachments checklist</Typography>
<Box sx={{ 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"
/>
<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" />
</Box>
</Box>
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end", mt: 1 }}>
<Button variant="contained" onClick={() => void createJob()} disabled={saving || !canSave}>
{saving ? "Adding..." : "Add job"}
</Button>
<Box sx={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mt: 1, flexWrap: "wrap" }}>
<FormControlLabel
control={<Checkbox checked={saveAndAddAnother} onChange={(e) => setSaveAndAddAnother(e.target.checked)} />}
label="Save and add another"
/>
<Box sx={{ display: "flex", gap: 1 }}>
<Button variant="outlined" onClick={onClose}>Cancel</Button>
<Button variant="contained" onClick={() => void createJob()} disabled={saving || !canSave}>
{saving ? "Adding..." : saveAndAddAnother ? "Save and continue" : "Add job"}
</Button>
</Box>
</Box>
</Box>
</DialogContent>