feat(applications): guide job creation in steps
This commit is contained in:
@@ -18,6 +18,9 @@ import {
|
||||
ListItem,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Step,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
@@ -103,6 +106,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
const { t, language } = useI18n();
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [duplicateCheck, setDuplicateCheck] = useState<DuplicateCheckResult | null>(null);
|
||||
|
||||
@@ -154,6 +158,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
}, [open, initialUrl]);
|
||||
|
||||
const resetForm = () => {
|
||||
setActiveStep(0);
|
||||
setCompany(null);
|
||||
setCompanyInput("");
|
||||
setNewCompanyLocation("");
|
||||
@@ -163,6 +168,10 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
setStatus("Applied");
|
||||
setLocation("");
|
||||
setSalary("");
|
||||
setSalaryMin("");
|
||||
setSalaryMax("");
|
||||
setSalaryCurrency("");
|
||||
setSalaryPeriod("");
|
||||
setJobUrl("");
|
||||
setDeadline("");
|
||||
setDescription("");
|
||||
@@ -272,6 +281,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
setDeadline(r.deadline ? r.deadline.slice(0, 10) : "");
|
||||
|
||||
toast(t("addJobModalImported"), "success");
|
||||
setActiveStep(1);
|
||||
} catch (e: any) {
|
||||
toast(e?.message || t("addJobModalImportFailed"), "error");
|
||||
} finally {
|
||||
@@ -355,6 +365,8 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
};
|
||||
|
||||
const canSave = normalizedCompanyName.length > 0 && jobTitle.trim().length > 0;
|
||||
const steps = ["Add job", "Review details", "CV", "Cover letter", "Portfolio", "Additional files"];
|
||||
const optionalStep = activeStep >= 2 && activeStep <= 5;
|
||||
|
||||
const setFilesForBucket = (bucket: AttachmentBucketKey, files: FileList | null) => {
|
||||
setAttachments((prev) => ({
|
||||
@@ -400,6 +412,30 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Stepper activeStep={activeStep} alternativeLabel sx={{ mb: 3, display: { xs: "none", sm: "flex" } }}>
|
||||
{steps.map((label) => <Step key={label}><StepLabel>{label}</StepLabel></Step>)}
|
||||
</Stepper>
|
||||
<Typography variant="subtitle2" sx={{ display: { xs: "block", sm: "none" }, mb: 2 }}>
|
||||
Step {activeStep + 1} of {steps.length}: {steps[activeStep]}
|
||||
</Typography>
|
||||
|
||||
{activeStep === 0 ? <Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>Start with the job advert</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.5 }}>
|
||||
Paste a job URL and Jobbjakt will try to fill the details. You can continue with manual entry whenever extraction is unavailable.
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField label={t("addJobModalJobUrl")} value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} fullWidth sx={FIELD_SX} />
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button variant="contained" onClick={() => void importFromUrl()} disabled={importing || !jobUrl.trim()}>
|
||||
{importing ? t("addJobModalImporting") : t("addJobModalImportFromUrl")}
|
||||
</Button>
|
||||
<Button onClick={() => setActiveStep(1)}>Enter details manually</Button>
|
||||
</Box>
|
||||
</Box> : null}
|
||||
|
||||
{activeStep === 1 ? <>
|
||||
<Typography variant="overline" sx={{ display: "block", mt: 0.5 }}>
|
||||
{t("addJobModalCompanySection")}
|
||||
</Typography>
|
||||
@@ -466,11 +502,6 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2, mt: 1 }}>
|
||||
<TextField label={t("addJobModalJobUrl")} value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} sx={{ ...FIELD_SX, gridColumn: "1 / -1" }} />
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button onClick={() => void importFromUrl()} disabled={importing || !jobUrl.trim()}>
|
||||
{importing ? t("addJobModalImporting") : t("addJobModalImportFromUrl")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<DatePicker
|
||||
label={t("addJobModalDateApplied")}
|
||||
@@ -543,31 +574,43 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
|
||||
<TextField label={t("addJobModalNotes")} multiline rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} helperText={`${notes.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline" sx={{ display: "block", mb: 1 }}>{t("addJobModalDocuments")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||
{uploadField("resume", t("addJobModalResume"), t("addJobModalResumeHelp"))}
|
||||
{uploadField("coverLetter", t("addJobModalCoverLetter"), t("addJobModalCoverLetterHelp"))}
|
||||
{uploadField("portfolio", t("addJobModalPortfolio"), t("addJobModalPortfolioHelp"))}
|
||||
{uploadField("other", t("addJobModalOtherFiles"), t("addJobModalOtherFilesHelp"))}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.25 }}>
|
||||
<Chip size="small" variant="outlined" label={attachmentCount === 1 ? t("addJobModalFileReady", { count: attachmentCount }) : t("addJobModalFilesReady", { count: attachmentCount })} />
|
||||
<Chip size="small" variant="outlined" label={t("addJobModalPreferredFiles")} />
|
||||
<Chip size="small" variant="outlined" label={t("addJobModalTextImageAllowed")} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</> : null}
|
||||
|
||||
{activeStep >= 2 ? <Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{steps[activeStep]}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.5 }}>
|
||||
{activeStep === 2 ? "Use your master CV later in Career Workspace, or attach the version prepared for this application now." : null}
|
||||
{activeStep === 3 ? "Attach a drafted cover letter now, or add/generate one from the application workspace later." : null}
|
||||
{activeStep === 4 ? "Add supporting portfolio material now, or select lightweight profile-linked projects later." : null}
|
||||
{activeStep === 5 ? "Certificates, references, and other supporting documents are optional." : null}
|
||||
</Typography>
|
||||
</Box>
|
||||
{activeStep === 2 ? uploadField("resume", t("addJobModalResume"), t("addJobModalResumeHelp")) : null}
|
||||
{activeStep === 3 ? uploadField("coverLetter", t("addJobModalCoverLetter"), t("addJobModalCoverLetterHelp")) : null}
|
||||
{activeStep === 4 ? uploadField("portfolio", t("addJobModalPortfolio"), t("addJobModalPortfolioHelp")) : null}
|
||||
{activeStep === 5 ? uploadField("other", t("addJobModalOtherFiles"), t("addJobModalOtherFilesHelp")) : null}
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Chip size="small" variant="outlined" label={attachmentCount === 1 ? t("addJobModalFileReady", { count: attachmentCount }) : t("addJobModalFilesReady", { count: attachmentCount })} />
|
||||
<Chip size="small" variant="outlined" label={t("addJobModalPreferredFiles")} />
|
||||
</Box>
|
||||
</Box> : null}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, py: 2, justifyContent: "space-between", flexWrap: "wrap", gap: 1.5 }}>
|
||||
<Button variant="outlined" onClick={onClose}>{t("close")}</Button>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button variant="outlined" onClick={() => void createJob(true)} disabled={saving || !canSave}>
|
||||
{saving ? t("rulesSaving") : t("createAndAddAnother")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => void createJob(false)} disabled={saving || !canSave}>
|
||||
{saving ? t("rulesSaving") : t("createJob")}
|
||||
</Button>
|
||||
{activeStep > 0 ? <Button onClick={() => setActiveStep((step) => step - 1)} disabled={saving}>Back</Button> : null}
|
||||
{activeStep < steps.length - 1 ? <Button variant="contained" onClick={() => setActiveStep((step) => step + 1)} disabled={saving || (activeStep === 1 && !canSave)}>
|
||||
{optionalStep ? "Skip and continue" : "Continue"}
|
||||
</Button> : <>
|
||||
<Button variant="outlined" onClick={() => void createJob(true)} disabled={saving || !canSave}>
|
||||
{saving ? t("rulesSaving") : t("createAndAddAnother")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => void createJob(false)} disabled={saving || !canSave}>
|
||||
{saving ? t("rulesSaving") : t("createJob")}
|
||||
</Button>
|
||||
</>}
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user