Files
jobtrackingapp/job-tracker-ui/src/components/CompaniesTable.tsx
T

210 lines
7.5 KiB
TypeScript

import React, { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Typography,
IconButton,
} from "@mui/material";
import { api, getApiErrorMessage } from "../api";
import { Company } from "../types";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
export default function CompaniesTable() {
const { toast } = useToast();
const { t } = useI18n();
const location = useLocation();
const navigate = useNavigate();
const [companies, setCompanies] = useState<Company[]>([]);
const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<Company | null>(null);
const [recruiterName, setRecruiterName] = useState("");
const [recruiterEmail, setRecruiterEmail] = useState("");
const [recruiterLinkedIn, setRecruiterLinkedIn] = useState("");
const [pipelineStage, setPipelineStage] = useState("");
const [lastContactedAt, setLastContactedAt] = useState("");
const [nextContactAt, setNextContactAt] = useState("");
useEffect(() => {
api.get<Company[]>("/companies").then((r) => setCompanies(r.data)).catch((error) => toast(getApiErrorMessage(error, t("companiesUpdateFailed")), "error"));
}, [t, toast]);
useEffect(() => {
const params = new URLSearchParams(location.search);
const editId = Number(params.get("edit") || 0);
if (!editId || companies.length === 0) return;
const company = companies.find((c) => c.id === editId);
if (!company) return;
openEdit(company);
params.delete("edit");
navigate({ pathname: location.pathname, search: params.toString() ? `?${params.toString()}` : "" }, { replace: true });
}, [companies, location.pathname, location.search, navigate]);
const openEdit = (c: Company) => {
setEditing(c);
setRecruiterName(c.recruiterName ?? "");
setRecruiterEmail(c.recruiterEmail ?? "");
setRecruiterLinkedIn(c.recruiterLinkedIn ?? "");
setPipelineStage(c.pipelineStage ?? "");
setLastContactedAt((c.lastContactedAt ?? "").slice(0, 10));
setNextContactAt((c.nextContactAt ?? "").slice(0, 10));
setEditOpen(true);
};
const canSave = useMemo(() => !!editing?.id, [editing]);
const save = async () => {
if (!editing?.id) return;
try {
const res = await api.put<Company>(`/companies/${editing.id}`, {
name: editing.name,
location: editing.location ?? null,
source: editing.source ?? null,
recruiterName: recruiterName.trim() || null,
recruiterEmail: recruiterEmail.trim() || null,
recruiterLinkedIn: recruiterLinkedIn.trim() || null,
pipelineStage: pipelineStage.trim() || null,
lastContactedAt: lastContactedAt || null,
nextContactAt: nextContactAt || null,
});
setCompanies((prev) => prev.map((x) => (x.id === res.data.id ? res.data : x)));
toast(t("companiesUpdated"), "success");
setEditOpen(false);
setEditing(null);
} catch (error) {
toast(getApiErrorMessage(error, t("companiesUpdateFailed")), "error");
}
};
return (
<Paper sx={{ mt: 0 }}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("companiesName")}</TableCell>
<TableCell>{t("companiesLocation")}</TableCell>
<TableCell>{t("companiesSource")}</TableCell>
<TableCell>{t("companiesPipeline")}</TableCell>
<TableCell>{t("companiesRecruiter")}</TableCell>
<TableCell>{t("companiesNextContact")}</TableCell>
<TableCell width={1} align="right" />
</TableRow>
</TableHead>
<TableBody>
{companies.map((c) => (
<TableRow key={c.id}>
<TableCell>{c.name}</TableCell>
<TableCell>{c.location ?? ""}</TableCell>
<TableCell>{c.source ?? ""}</TableCell>
<TableCell>{c.pipelineStage ?? ""}</TableCell>
<TableCell>
{c.recruiterName ?? ""}
{c.recruiterEmail ? ` (${c.recruiterEmail})` : ""}
</TableCell>
<TableCell>{c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""}</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => openEdit(c)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
{companies.length === 0 && (
<TableRow>
<TableCell colSpan={7}>
<Typography sx={{ py: 2, textAlign: "center" }}>
{t("companiesEmpty")}
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<Dialog open={editOpen} onClose={() => setEditOpen(false)} fullWidth maxWidth="sm">
<DialogTitle>{t("companiesEdit")}</DialogTitle>
<DialogContent>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, mt: 1 }}>
<TextField
label={t("companiesName")}
value={editing?.name ?? ""}
onChange={(e) => setEditing((p) => (p ? { ...p, name: e.target.value } : p))}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
label={t("companiesLocation")}
value={editing?.location ?? ""}
onChange={(e) => setEditing((p) => (p ? { ...p, location: e.target.value } : p))}
/>
<TextField
label={t("companiesSource")}
value={editing?.source ?? ""}
onChange={(e) => setEditing((p) => (p ? { ...p, source: e.target.value } : p))}
/>
<TextField
label={t("companiesPipelineStage")}
value={pipelineStage}
onChange={(e) => setPipelineStage(e.target.value)}
/>
<TextField
label={t("companiesRecruiterName")}
value={recruiterName}
onChange={(e) => setRecruiterName(e.target.value)}
/>
<TextField
label={t("companiesRecruiterEmail")}
value={recruiterEmail}
onChange={(e) => setRecruiterEmail(e.target.value)}
/>
<TextField
label={t("companiesRecruiterLinkedIn")}
value={recruiterLinkedIn}
onChange={(e) => setRecruiterLinkedIn(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
/>
<TextField
label={t("companiesLastContacted")}
type="date"
value={lastContactedAt}
onChange={(e) => setLastContactedAt(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<TextField
label={t("companiesNextContactField")}
type="date"
value={nextContactAt}
onChange={(e) => setNextContactAt(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditOpen(false)}>{t("cancel")}</Button>
<Button variant="contained" onClick={save} disabled={!canSave}>
{t("save")}
</Button>
</DialogActions>
</Dialog>
</Paper>
);
}