import React, { useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { Box, Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from "@mui/material"; 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 PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; 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)"); const { toast } = useToast(); const { language, t } = useI18n(); const location = useLocation(); const navigate = useNavigate(); const [editOpen, setEditOpen] = useState(false); const [editing, setEditing] = useState(null); const [relationshipOpen, setRelationshipOpen] = useState(false); const [relationshipLoading, setRelationshipLoading] = useState(false); const [relationshipError, setRelationshipError] = useState(""); const [relationship, setRelationship] = useState(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(""); const companiesResource = useViewResource( async () => { const response = await api.get("/companies"); return response.data; }, { initialData: [], errorMessage: t("companiesLoadFailed"), deps: [t], }, ); const companies = companiesResource.data; 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 openRelationship = async (company: Company) => { setRelationshipOpen(true); setRelationshipLoading(true); setRelationshipError(""); setRelationship(null); try { const response = await api.get(`/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 () => { if (!editing?.id) return; try { const res = await api.put(`/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, }); companiesResource.setData((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"); } }; const renderCompanyMeta = (label: string, value?: string | null) => ( {label} {value || "—"} ); return ( {!companiesResource.loading && !companiesResource.error ? ( isMobile ? ( {companies.map((c) => ( {c.name} {c.location || t("companiesLocation")} openEdit(c)}> void openRelationship(c)}> {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)} ))} {companies.length === 0 ? ( {t("companiesEmpty")} ) : null} ) : ( {t("companiesName")} {t("companiesLocation")} {t("companiesSource")} {t("companiesPipeline")} {t("companiesRecruiter")} {t("companiesNextContact")} {companies.map((c) => ( {c.name} {c.location ?? ""} {c.source ?? ""} {c.pipelineStage ?? ""} {c.recruiterName ?? ""} {c.recruiterEmail ? ` (${c.recruiterEmail})` : ""} {c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""} void openRelationship(c)}> openEdit(c)}> ))} {companies.length === 0 && ( {t("companiesEmpty")} )}
) ) : null} setEditOpen(false)} fullWidth fullScreen={isMobile} maxWidth="sm"> {t("companiesEdit")} setEditing((p) => (p ? { ...p, name: e.target.value } : p))} sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> setEditing((p) => (p ? { ...p, location: e.target.value } : p))} /> setEditing((p) => (p ? { ...p, source: e.target.value } : p))} /> setPipelineStage(e.target.value)} /> setRecruiterName(e.target.value)} /> setRecruiterEmail(e.target.value)} /> setRecruiterLinkedIn(e.target.value)} sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} /> setLastContactedAt(e.target.value)} InputLabelProps={{ shrink: true }} /> setNextContactAt(e.target.value)} InputLabelProps={{ shrink: true }} /> setRelationshipOpen(false)} fullWidth fullScreen={isMobile} maxWidth="md"> {relationship?.company.name ?? t("companiesRelationship")} {relationshipLoading ? {t("loading")} : null} {relationshipError ? {relationshipError} : null} {relationship ? ( {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)} {t("companiesApplications")} {relationship.applications.length === 0 ? {t("companiesNoApplications")} : ( {relationship.applications.map((application) => ( {application.jobTitle}{application.status} · {t("companiesMessageCount", { count: application.messageCount })} ))} )} {t("companiesRecentContact")} {relationship.recentContacts.length === 0 ? {t("companiesNoContact")} : ( {relationship.recentContacts.map((contact) => ( {contact.subject || contact.channel || t("companiesContact")} {contact.jobTitle} · {new Date(contact.date).toLocaleDateString(language === "nb" ? "nb-NO" : "en")} · {contact.from} ))} )} ) : null}
); } 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 }>; };