feat: structured salary fields (min/max/currency/period)

Adds SalaryMin/SalaryMax/SalaryCurrency/SalaryPeriod alongside the
existing free-text Salary field (kept for back-compat and display).

- JobApplication model + idempotent column bridging for SQLite and MySQL
- Create/Update DTOs with NormalizeSalary (clamps negatives, swaps
  inverted min/max, uppercases currency, whitelists period)
- JobApplicationDto exposes the fields; CSV export gains 4 columns
- UI: add/edit dialogs get min/max/currency/period inputs; job table
  renders a formatted range via shared salary.ts formatter (falls back
  to free-text when structured values are absent)
- EN/NB translations; backend + full frontend suites green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-02 22:15:37 +02:00
parent 8f174cb767
commit 83e6430a24
11 changed files with 250 additions and 2 deletions
@@ -118,6 +118,10 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
const [status, setStatus] = useState<(typeof STATUS_OPTIONS)[number]>("Applied");
const [location, setLocation] = useState("");
const [salary, setSalary] = useState("");
const [salaryMin, setSalaryMin] = useState("");
const [salaryMax, setSalaryMax] = useState("");
const [salaryCurrency, setSalaryCurrency] = useState("");
const [salaryPeriod, setSalaryPeriod] = useState("");
const [jobUrl, setJobUrl] = useState("");
const [deadline, setDeadline] = useState("");
@@ -291,6 +295,10 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
status,
location,
salary,
salaryMin: salaryMin.trim() ? Number(salaryMin) : null,
salaryMax: salaryMax.trim() ? Number(salaryMax) : null,
salaryCurrency: salaryCurrency.trim() || null,
salaryPeriod: salaryPeriod || null,
nextAction: null,
followUpAt: null,
jobUrl,
@@ -482,6 +490,15 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
<TextField label={t("location")} value={location} onChange={(e) => setLocation(e.target.value)} sx={FIELD_SX} />
<TextField label={t("addJobModalSalary")} value={salary} onChange={(e) => setSalary(e.target.value)} sx={FIELD_SX} />
<TextField label={t("salaryMinLabel")} type="number" value={salaryMin} onChange={(e) => setSalaryMin(e.target.value)} sx={FIELD_SX} />
<TextField label={t("salaryMaxLabel")} type="number" value={salaryMax} onChange={(e) => setSalaryMax(e.target.value)} sx={FIELD_SX} />
<TextField label={t("salaryCurrencyLabel")} value={salaryCurrency} onChange={(e) => setSalaryCurrency(e.target.value)} sx={FIELD_SX} inputProps={{ maxLength: 8 }} />
<TextField select SelectProps={{ native: true }} label={t("salaryPeriodLabel")} value={salaryPeriod} onChange={(e) => setSalaryPeriod(e.target.value)} sx={FIELD_SX} InputLabelProps={{ shrink: true }}>
<option value=""></option>
<option value="year">{t("salaryPeriodYear")}</option>
<option value="month">{t("salaryPeriodMonth")}</option>
<option value="hour">{t("salaryPeriodHour")}</option>
</TextField>
<DatePicker
label={t("addJobModalDeadline")}
value={parsePickerDate(deadline)}