refactor, security updates, cv extraction upgrades
This commit is contained in:
@@ -23,10 +23,12 @@ import {
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import ViewStateNotice from "./ViewStateNotice";
|
||||
import { Company } from "../types";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useViewResource } from "../hooks/useViewResource";
|
||||
|
||||
export default function CompaniesTable() {
|
||||
const isMobile = useMediaQuery("(max-width:767.95px)");
|
||||
@@ -34,7 +36,6 @@ export default function CompaniesTable() {
|
||||
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);
|
||||
|
||||
@@ -45,9 +46,19 @@ export default function CompaniesTable() {
|
||||
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]);
|
||||
const companiesResource = useViewResource(
|
||||
async () => {
|
||||
const response = await api.get<Company[]>("/companies");
|
||||
return response.data;
|
||||
},
|
||||
{
|
||||
initialData: [],
|
||||
errorMessage: t("companiesUpdateFailed"),
|
||||
deps: [t],
|
||||
},
|
||||
);
|
||||
|
||||
const companies = companiesResource.data;
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
@@ -88,7 +99,7 @@ export default function CompaniesTable() {
|
||||
nextContactAt: nextContactAt || null,
|
||||
});
|
||||
|
||||
setCompanies((prev) => prev.map((x) => (x.id === res.data.id ? res.data : x)));
|
||||
companiesResource.setData((prev) => prev.map((x) => (x.id === res.data.id ? res.data : x)));
|
||||
toast(t("companiesUpdated"), "success");
|
||||
setEditOpen(false);
|
||||
setEditing(null);
|
||||
@@ -106,82 +117,92 @@ export default function CompaniesTable() {
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 0, p: { xs: 1.5, sm: 0 } }}>
|
||||
{isMobile ? (
|
||||
<Stack spacing={1.5}>
|
||||
{companies.map((c) => (
|
||||
<Paper key={c.id} sx={{ p: 1.5, borderRadius: 3 }}>
|
||||
<Stack spacing={1.25}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 900, overflowWrap: "anywhere" }}>{c.name}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>{c.location || t("companiesLocation")}</Typography>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={() => openEdit(c)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ViewStateNotice
|
||||
loading={companiesResource.loading}
|
||||
error={companiesResource.error}
|
||||
title="Unable to load companies"
|
||||
description="The companies list is unavailable right now. Try again when the API is reachable."
|
||||
onRetry={companiesResource.reload}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
|
||||
{renderCompanyMeta(t("companiesSource"), c.source)}
|
||||
{renderCompanyMeta(t("companiesPipeline"), c.pipelineStage)}
|
||||
{renderCompanyMeta(t("companiesRecruiter"), [c.recruiterName, c.recruiterEmail].filter(Boolean).join(" · "))}
|
||||
{renderCompanyMeta(t("companiesNextContact"), c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : null)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
{companies.length === 0 ? (
|
||||
<Typography sx={{ py: 2, textAlign: "center" }}>
|
||||
{t("companiesEmpty")}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
) : (
|
||||
<TableContainer sx={{ borderRadius: 3, border: "1px solid", borderColor: "divider" }}>
|
||||
<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">
|
||||
{!companiesResource.loading && !companiesResource.error ? (
|
||||
isMobile ? (
|
||||
<Stack spacing={1.5}>
|
||||
{companies.map((c) => (
|
||||
<Paper key={c.id} sx={{ p: 1.5, borderRadius: 3 }}>
|
||||
<Stack spacing={1.25}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 900, overflowWrap: "anywhere" }}>{c.name}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>{c.location || t("companiesLocation")}</Typography>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={() => openEdit(c)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{companies.length === 0 && (
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
|
||||
{renderCompanyMeta(t("companiesSource"), c.source)}
|
||||
{renderCompanyMeta(t("companiesPipeline"), c.pipelineStage)}
|
||||
{renderCompanyMeta(t("companiesRecruiter"), [c.recruiterName, c.recruiterEmail].filter(Boolean).join(" · "))}
|
||||
{renderCompanyMeta(t("companiesNextContact"), c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : null)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
{companies.length === 0 ? (
|
||||
<Typography sx={{ py: 2, textAlign: "center" }}>
|
||||
{t("companiesEmpty")}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
) : (
|
||||
<TableContainer sx={{ borderRadius: 3, border: "1px solid", borderColor: "divider" }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell colSpan={7}>
|
||||
<Typography sx={{ py: 2, textAlign: "center" }}>
|
||||
{t("companiesEmpty")}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<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>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</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>
|
||||
</TableContainer>
|
||||
)
|
||||
) : null}
|
||||
|
||||
<Dialog open={editOpen} onClose={() => setEditOpen(false)} fullWidth fullScreen={isMobile} maxWidth="sm">
|
||||
<DialogTitle>{t("companiesEdit")}</DialogTitle>
|
||||
|
||||
Reference in New Issue
Block a user