feat(companies): add recruiter relationship history

This commit is contained in:
cesnimda
2026-08-31 17:25:31 +02:00
parent 7d5a78ad5d
commit f3569d05a9
6 changed files with 201 additions and 1 deletions
@@ -3,6 +3,7 @@ import { useLocation, useNavigate } from "react-router-dom";
import {
Box,
Alert,
Button,
Dialog,
DialogActions,
@@ -26,6 +27,7 @@ import { api, getApiErrorMessage } from "../api";
import ViewStateNotice from "./ViewStateNotice";
import { Company } from "../types";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
import { useViewResource } from "../hooks/useViewResource";
@@ -33,11 +35,15 @@ import { useViewResource } from "../hooks/useViewResource";
export default function CompaniesTable() {
const isMobile = useMediaQuery("(max-width:767.95px)");
const { toast } = useToast();
const { t } = useI18n();
const { language, t } = useI18n();
const location = useLocation();
const navigate = useNavigate();
const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<Company | null>(null);
const [relationshipOpen, setRelationshipOpen] = useState(false);
const [relationshipLoading, setRelationshipLoading] = useState(false);
const [relationshipError, setRelationshipError] = useState("");
const [relationship, setRelationship] = useState<CompanyRelationship | null>(null);
const [recruiterName, setRecruiterName] = useState("");
const [recruiterEmail, setRecruiterEmail] = useState("");
@@ -82,6 +88,18 @@ export default function CompaniesTable() {
setEditOpen(true);
};
const openRelationship = async (company: Company) => {
setRelationshipOpen(true); setRelationshipLoading(true); setRelationshipError(""); setRelationship(null);
try {
const response = await api.get<CompanyRelationship>(`/companies/${company.id}/relationship`);
setRelationship(response.data);
} catch (error) {
setRelationshipError(getApiErrorMessage(error, t("companiesRelationshipLoadFailed")));
} finally {
setRelationshipLoading(false);
}
};
const canSave = useMemo(() => !!editing?.id, [editing]);
const save = async () => {
@@ -139,6 +157,9 @@ export default function CompaniesTable() {
<IconButton size="small" aria-label={`${t("jobTableEdit")}: ${c.name}`} onClick={() => openEdit(c)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<IconButton size="small" aria-label={`${t("companiesRelationship")}: ${c.name}`} onClick={() => void openRelationship(c)}>
<PeopleAltOutlinedIcon fontSize="small" />
</IconButton>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
@@ -183,6 +204,9 @@ export default function CompaniesTable() {
</TableCell>
<TableCell>{c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""}</TableCell>
<TableCell align="right">
<IconButton size="small" aria-label={`${t("companiesRelationship")}: ${c.name}`} onClick={() => void openRelationship(c)}>
<PeopleAltOutlinedIcon fontSize="small" />
</IconButton>
<IconButton size="small" aria-label={`${t("jobTableEdit")}: ${c.name}`} onClick={() => openEdit(c)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
@@ -270,6 +294,52 @@ export default function CompaniesTable() {
</Button>
</DialogActions>
</Dialog>
<Dialog open={relationshipOpen} onClose={() => setRelationshipOpen(false)} fullWidth fullScreen={isMobile} maxWidth="md">
<DialogTitle>{relationship?.company.name ?? t("companiesRelationship")}</DialogTitle>
<DialogContent>
{relationshipLoading ? <Typography color="text.secondary">{t("loading")}</Typography> : null}
{relationshipError ? <Alert severity="error">{relationshipError}</Alert> : null}
{relationship ? (
<Stack spacing={2} sx={{ mt: 0.5 }}>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(3, minmax(0, 1fr))" }, gap: 1.5 }}>
{renderCompanyMeta(t("companiesRecruiter"), [relationship.company.recruiterName, relationship.company.recruiterEmail].filter(Boolean).join(" · "))}
{renderCompanyMeta(t("companiesLastContacted"), relationship.company.lastContactedAt ? new Date(relationship.company.lastContactedAt).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : null)}
{renderCompanyMeta(t("companiesNextContact"), relationship.company.nextContactAt ? new Date(relationship.company.nextContactAt).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : null)}
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("companiesApplications")}</Typography>
{relationship.applications.length === 0 ? <Typography variant="body2" color="text.secondary">{t("companiesNoApplications")}</Typography> : (
<Stack spacing={1}>{relationship.applications.map((application) => (
<Paper key={application.id} variant="outlined" sx={{ p: 1.25, display: "flex", flexWrap: "wrap", gap: 1, alignItems: "center" }}>
<Box sx={{ flex: "1 1 220px" }}><Typography sx={{ fontWeight: 700 }}>{application.jobTitle}</Typography><Typography variant="caption" color="text.secondary">{application.status} · {t("companiesMessageCount", { count: application.messageCount })}</Typography></Box>
<Button size="small" onClick={() => { setRelationshipOpen(false); navigate(`/jobs/${application.id}`); }}>{t("notificationsOpen")}</Button>
</Paper>
))}</Stack>
)}
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("companiesRecentContact")}</Typography>
{relationship.recentContacts.length === 0 ? <Typography variant="body2" color="text.secondary">{t("companiesNoContact")}</Typography> : (
<Stack spacing={1}>{relationship.recentContacts.map((contact) => (
<Box key={contact.id} sx={{ pb: 1, borderBottom: 1, borderColor: "divider" }}>
<Typography variant="body2" sx={{ fontWeight: 700 }}>{contact.subject || contact.channel || t("companiesContact")}</Typography>
<Typography variant="caption" color="text.secondary">{contact.jobTitle} · {new Date(contact.date).toLocaleDateString(language === "nb" ? "nb-NO" : "en")} · {contact.from}</Typography>
</Box>
))}</Stack>
)}
</Box>
</Stack>
) : null}
</DialogContent>
<DialogActions><Button onClick={() => setRelationshipOpen(false)}>{t("close")}</Button></DialogActions>
</Dialog>
</Paper>
);
}
type CompanyRelationship = {
company: Company;
applications: Array<{ id: number; jobTitle: string; status: string; dateApplied?: string; followUpAt?: string; messageCount: number; lastContactAt?: string }>;
recentContacts: Array<{ id: number; jobApplicationId: number; jobTitle: string; date: string; direction?: string; channel?: string; subject?: string; from: string }>;
};