import React, { useMemo, useState } from "react"; import { Box, Button, Paper, Typography } from "@mui/material"; import { api } from "../api"; import { Company, JobApplication } from "../types"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; type ImportJob = Omit & { company: Pick; }; function downloadText(filename: string, text: string, mime: string) { const blob = new Blob([text], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 0); } function parseCsv(text: string): string[][] { const rows: string[][] = []; let row: string[] = []; let cur = ""; let i = 0; let inQuotes = false; const pushCell = () => { row.push(cur); cur = ""; }; const pushRow = () => { if (row.length === 1 && row[0] === "" && rows.length > 0) { row = []; return; } rows.push(row); row = []; }; while (i < text.length) { const ch = text[i]; if (inQuotes) { if (ch === '"') { const next = text[i + 1]; if (next === '"') { cur += '"'; i += 2; continue; } inQuotes = false; i += 1; continue; } cur += ch; i += 1; continue; } if (ch === '"') { inQuotes = true; i += 1; continue; } if (ch === ",") { pushCell(); i += 1; continue; } if (ch === "\r") { i += 1; continue; } if (ch === "\n") { pushCell(); pushRow(); i += 1; continue; } cur += ch; i += 1; } pushCell(); pushRow(); return rows; } function parseDateDMY(s: string): string | null { const v = (s || "").trim(); if (!v) return null; const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(v); if (!m) return null; const dd = Number(m[1]); const mm = Number(m[2]); const yyyy = Number(m[3]); if (!yyyy || mm < 1 || mm > 12 || dd < 1 || dd > 31) return null; return `${String(yyyy).padStart(4, "0")}-${String(mm).padStart(2, "0")}-${String(dd).padStart(2, "0")}`; } function csvToImportJobs(csvText: string): ImportJob[] { const rows = parseCsv(csvText); if (!rows.length) return []; const header = rows[0].map((h) => h.trim()); const idx = (name: string) => header.findIndex((h) => h.toLowerCase() === name.toLowerCase()); const iJobTitle = idx("Job Title"); const iCompany = idx("Company"); const iLocation = idx("Location"); const iSource = idx("Source"); const iDateApplied = idx("Date Applied"); const iMethod = idx("Application Method"); const iCover = idx("Cover Letter / Resume"); const iResp = idx("Response Received"); const iRespDate = idx("Response Date"); const iStatus = idx("Status"); const iNotes = idx("Notes"); const iInterviewDate = idx("Interview Date"); const out: ImportJob[] = []; for (let r = 1; r < rows.length; r++) { const row = rows[r]; const get = (i: number) => (i >= 0 ? (row[i] ?? "").trim() : ""); const jobTitle = get(iJobTitle); const companyName = get(iCompany) || jobTitle; if (!jobTitle && !companyName) continue; const rawStatus = get(iStatus); const status = rawStatus === "Follow-up Needed" ? "Waiting" : rawStatus || "Applied"; const dateApplied = parseDateDMY(get(iDateApplied)) ?? new Date().toISOString().slice(0, 10); const responseReceived = /^yes$/i.test(get(iResp)); const responseDate = parseDateDMY(get(iRespDate)) ?? undefined; const interviewDate = parseDateDMY(get(iInterviewDate)) ?? ""; const notesParts: string[] = []; const baseNotes = get(iNotes); if (baseNotes) notesParts.push(baseNotes); const method = get(iMethod); if (method) notesParts.push(`Application Method: ${method}`); const cover = get(iCover); if (cover) notesParts.push(`Cover Letter / Resume: ${cover}`); if (interviewDate) notesParts.push(`Interview Date: ${interviewDate}`); if (responseDate) notesParts.push(`Response Date: ${responseDate}`); out.push({ jobTitle: jobTitle || "(missing title)", status, dateApplied, responseReceived, responseDate, notes: notesParts.join("\n") || undefined, coverLetterText: undefined, jobUrl: undefined, daysSince: 0, company: { name: companyName || "(missing company)", location: get(iLocation) || undefined, source: get(iSource) || undefined, }, location: get(iLocation) || undefined, salary: undefined, nextAction: status === "Waiting" ? "Follow up" : undefined, followUpAt: undefined, isDeleted: false, deletedAt: undefined, }); } return out; } export default function ImportExportJobs() { const { toast } = useToast(); const { t } = useI18n(); const [importing, setImporting] = useState(false); const [lastImportCount, setLastImportCount] = useState(null); const exportAll = async (format: "json" | "csv") => { try { const stamp = new Date().toISOString().slice(0, 10); const url = `/export/jobs?format=${format}&includeDeleted=false`; const res = await api.get(url, { responseType: "blob" }); const text = await res.data.text(); downloadText(`job-tracker-export-${stamp}.${format}`, text, format === "json" ? "application/json" : "text/csv"); toast(t("exportedJobs", { format: format.toUpperCase() }), "success"); } catch { toast(t("exportFailed"), "error"); } }; const onImportFile = async (file: File | null) => { if (!file) return; setImporting(true); setLastImportCount(null); try { const text = await file.text(); const parsed = JSON.parse(text) as ImportJob[]; if (!Array.isArray(parsed)) throw new Error("Invalid format"); let created = 0; for (const j of parsed) { const companyRes = await api.post("/companies", j.company); const company = companyRes.data; await api.post("/jobapplications", { jobTitle: j.jobTitle, companyId: company.id, status: j.status, dateApplied: j.dateApplied, location: j.location, salary: j.salary, nextAction: j.nextAction, followUpAt: j.followUpAt, jobUrl: j.jobUrl, notes: j.notes, coverLetterText: j.coverLetterText, }); created += 1; } setLastImportCount(created); toast(t("importedJobs", { count: created }), "success"); } catch { toast(t("importFailedJson"), "error"); } finally { setImporting(false); } }; const onConvertCsv = async (file: File | null) => { if (!file) return; try { const text = await file.text(); const jobs = csvToImportJobs(text); const stamp = new Date().toISOString().slice(0, 10); downloadText(`job-tracker-import-${stamp}.json`, JSON.stringify(jobs, null, 2), "application/json"); toast(t("convertedRows", { count: jobs.length }), "success"); } catch { toast(t("csvConversionFailed"), "error"); } }; const helper = useMemo(() => t("importExportBody"), [t]); return ( {t("importExportTitle")} {helper} {lastImportCount !== null && ( {t("lastImport", { count: lastImportCount })} )} ); }