feat(companies): add recruiter relationship history
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using JobTrackerApi.Controllers;
|
||||||
|
using JobTrackerApi.Data;
|
||||||
|
using JobTrackerApi.Models;
|
||||||
|
using JobTrackerApi.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace JobTrackerApi.Tests;
|
||||||
|
|
||||||
|
public sealed class CompanyRelationshipTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Relationship_combines_owned_applications_and_contact_history()
|
||||||
|
{
|
||||||
|
var current = new Mock<ICurrentUserService>(); current.SetupGet(x => x.UserId).Returns("owner-1");
|
||||||
|
await using var db = new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options, current.Object);
|
||||||
|
var company = new Company { OwnerUserId = "owner-1", Name = "Acme", RecruiterName = "Rita" };
|
||||||
|
db.Companies.Add(company);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var application = new JobApplication { OwnerUserId = "owner-1", CompanyId = company.Id, JobTitle = "Backend Developer", Status = "Interview", DateApplied = new DateTime(2026, 8, 1) };
|
||||||
|
db.JobApplications.Add(application);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.Correspondences.Add(new Correspondence { JobApplicationId = application.Id, From = "Rita", Direction = "inbound", Channel = "email", Subject = "Interview", Content = "See you Tuesday", Date = new DateTime(2026, 8, 10) });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var controller = new CompaniesController(db) { ControllerContext = new ControllerContext { HttpContext = Http("owner-1") } };
|
||||||
|
|
||||||
|
var dto = Assert.IsType<CompaniesController.CompanyRelationshipDto>(Assert.IsType<OkObjectResult>((await controller.GetRelationship(company.Id, default)).Result).Value);
|
||||||
|
|
||||||
|
Assert.Equal("Acme", dto.Company.Name);
|
||||||
|
Assert.Equal(1, Assert.Single(dto.Applications).MessageCount);
|
||||||
|
Assert.Equal("Interview", Assert.Single(dto.RecentContacts).Subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Relationship_does_not_expose_another_owners_company()
|
||||||
|
{
|
||||||
|
var current = new Mock<ICurrentUserService>(); current.SetupGet(x => x.UserId).Returns("owner-1");
|
||||||
|
await using var db = new JobTrackerContext(new DbContextOptionsBuilder<JobTrackerContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options, current.Object);
|
||||||
|
var company = new Company { OwnerUserId = "owner-2", Name = "Private" };
|
||||||
|
db.Companies.Add(company);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var controller = new CompaniesController(db) { ControllerContext = new ControllerContext { HttpContext = Http("owner-1") } };
|
||||||
|
|
||||||
|
Assert.IsType<NotFoundResult>((await controller.GetRelationship(company.Id, default)).Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultHttpContext Http(string userId)
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, userId)], "test"));
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,31 @@ namespace JobTrackerApi.Controllers
|
|||||||
return Ok(company);
|
return Ok(company);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record CompanyApplicationRelationshipDto(int Id, string JobTitle, string Status, DateTime? DateApplied, DateTime? FollowUpAt, int MessageCount, DateTime? LastContactAt);
|
||||||
|
public sealed record CompanyContactDto(int Id, int JobApplicationId, string JobTitle, DateTime Date, string? Direction, string? Channel, string? Subject, string From);
|
||||||
|
public sealed record CompanyRelationshipDto(Company Company, IReadOnlyList<CompanyApplicationRelationshipDto> Applications, IReadOnlyList<CompanyContactDto> RecentContacts);
|
||||||
|
|
||||||
|
[HttpGet("{id:int}/relationship")]
|
||||||
|
public async Task<ActionResult<CompanyRelationshipDto>> GetRelationship([FromRoute] int id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = CurrentUserId;
|
||||||
|
if (string.IsNullOrWhiteSpace(userId)) return Unauthorized();
|
||||||
|
var company = await _db.Companies.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id && c.OwnerUserId == userId, cancellationToken);
|
||||||
|
if (company is null) return NotFound();
|
||||||
|
|
||||||
|
var applications = await _db.JobApplications.AsNoTracking().Include(a => a.Messages)
|
||||||
|
.Where(a => a.OwnerUserId == userId && a.CompanyId == id && !a.IsDeleted)
|
||||||
|
.OrderByDescending(a => a.DateApplied ?? a.SavedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var applicationRows = applications.Select(a => new CompanyApplicationRelationshipDto(
|
||||||
|
a.Id, a.JobTitle, a.Status, a.DateApplied, a.FollowUpAt, a.Messages.Count,
|
||||||
|
a.Messages.Count == 0 ? null : a.Messages.Max(m => (DateTime?)m.Date))).ToList();
|
||||||
|
var contacts = applications.SelectMany(a => a.Messages.Select(m => new CompanyContactDto(
|
||||||
|
m.Id, a.Id, a.JobTitle, m.Date, m.Direction, m.Channel, m.Subject, m.From)))
|
||||||
|
.OrderByDescending(m => m.Date).Take(20).ToList();
|
||||||
|
return Ok(new CompanyRelationshipDto(company, applicationRows, contacts));
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record CreateCompanyRequest(string Name, string? Location, string? Source);
|
public sealed record CreateCompanyRequest(string Name, string? Location, string? Source);
|
||||||
public sealed record UpdateCompanyRequest(
|
public sealed record UpdateCompanyRequest(
|
||||||
string Name,
|
string Name,
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ Updated: 2026-08-31
|
|||||||
- Career-evidence focused verification: backend/controller/schema gates passed 21/21, the evidence UI regression passed 1/1, ESLint passed with zero warnings, and the optimized Next build/TypeScript passed. Complete regression then passed backend 741/741 and frontend 61 suites with 262/262 tests.
|
- Career-evidence focused verification: backend/controller/schema gates passed 21/21, the evidence UI regression passed 1/1, ESLint passed with zero warnings, and the optimized Next build/TypeScript passed. Complete regression then passed backend 741/741 and frontend 61 suites with 262/262 tests.
|
||||||
- Saved job-search implementation: tenant-owned NAV search definitions now retain per-vacancy first-seen, last-seen and dismissal state; repeat runs identify only genuinely new results, while one-off discovery remains unchanged. Account export/deletion and provider-safe schema ownership include the new records. Focused backend tests passed 3/3, focused frontend tests passed 6/6, schema/migration gates passed 20/20, ESLint passed with zero warnings, and the optimized Next build/TypeScript passed.
|
- Saved job-search implementation: tenant-owned NAV search definitions now retain per-vacancy first-seen, last-seen and dismissal state; repeat runs identify only genuinely new results, while one-off discovery remains unchanged. Account export/deletion and provider-safe schema ownership include the new records. Focused backend tests passed 3/3, focused frontend tests passed 6/6, schema/migration gates passed 20/20, ESLint passed with zero warnings, and the optimized Next build/TypeScript passed.
|
||||||
- Interview debrief loop: interview-stage workspaces now create an explicit, durable debrief in the existing user-owned preparation model, prompting for questions, strengths, improvements, feedback and next steps without generating answers. It remains editable/deletable and is localized in English and Bokmål. Focused backend tests passed 11/11, focused frontend tests passed 13/13, ESLint and the optimized Next build/TypeScript passed.
|
- Interview debrief loop: interview-stage workspaces now create an explicit, durable debrief in the existing user-owned preparation model, prompting for questions, strengths, improvements, feedback and next steps without generating answers. It remains editable/deletable and is localized in English and Bokmål. Focused backend tests passed 11/11, focused frontend tests passed 13/13, ESLint and the optimized Next build/TypeScript passed.
|
||||||
|
- Recruiter relationship view: each company now exposes a tenant-scoped view over its existing recruiter details, active applications, status, message totals and recent correspondence, with direct application navigation and no duplicate contact store. Focused backend authorization/data tests passed 2/2, the frontend interaction test passed 1/1, ESLint and the optimized Next build/TypeScript passed.
|
||||||
- Focused frontend: 2 suites, 6 tests passed.
|
- Focused frontend: 2 suites, 6 tests passed.
|
||||||
- Full frontend: 64 suites, 272 tests passed.
|
- Full frontend: 64 suites, 272 tests passed.
|
||||||
- Next production build and TypeScript: passed.
|
- Next production build and TypeScript: passed.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { api } from "./api";
|
||||||
|
import CompaniesTable from "./components/CompaniesTable";
|
||||||
|
import { I18nProvider } from "./i18n/I18nProvider";
|
||||||
|
import { ToastProvider } from "./toast";
|
||||||
|
|
||||||
|
jest.mock("./api", () => ({
|
||||||
|
api: { get: jest.fn(), put: jest.fn() },
|
||||||
|
getApiErrorMessage: (_error: unknown, fallback: string) => fallback,
|
||||||
|
}));
|
||||||
|
const mockedApi = api as jest.Mocked<typeof api>;
|
||||||
|
|
||||||
|
test("opens one company relationship view with applications and recent contact", async () => {
|
||||||
|
const company = { id: 3, name: "Acme", recruiterName: "Rita", recruiterEmail: "rita@example.test" };
|
||||||
|
mockedApi.get.mockImplementation((url) => Promise.resolve({ data: String(url).endsWith("/relationship") ? {
|
||||||
|
company,
|
||||||
|
applications: [{ id: 9, jobTitle: "Backend Developer", status: "Interview", messageCount: 1 }],
|
||||||
|
recentContacts: [{ id: 12, jobApplicationId: 9, jobTitle: "Backend Developer", date: "2026-08-10T10:00:00Z", channel: "email", subject: "Interview", from: "Rita" }],
|
||||||
|
} : [company] } as any));
|
||||||
|
|
||||||
|
render(<I18nProvider><ToastProvider><MemoryRouter><CompaniesTable /></MemoryRouter></ToastProvider></I18nProvider>);
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Relationship history: Acme" }));
|
||||||
|
|
||||||
|
expect(await screen.findByText("Applications at this company")).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText("Backend Developer").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getByText(/Interview · 1 messages/)).toBeInTheDocument();
|
||||||
|
expect(mockedApi.get).toHaveBeenCalledWith("/companies/3/relationship");
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@@ -26,6 +27,7 @@ import { api, getApiErrorMessage } from "../api";
|
|||||||
import ViewStateNotice from "./ViewStateNotice";
|
import ViewStateNotice from "./ViewStateNotice";
|
||||||
import { Company } from "../types";
|
import { Company } from "../types";
|
||||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||||
|
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
|
||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import { useI18n } from "../i18n/I18nProvider";
|
import { useI18n } from "../i18n/I18nProvider";
|
||||||
import { useViewResource } from "../hooks/useViewResource";
|
import { useViewResource } from "../hooks/useViewResource";
|
||||||
@@ -33,11 +35,15 @@ import { useViewResource } from "../hooks/useViewResource";
|
|||||||
export default function CompaniesTable() {
|
export default function CompaniesTable() {
|
||||||
const isMobile = useMediaQuery("(max-width:767.95px)");
|
const isMobile = useMediaQuery("(max-width:767.95px)");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useI18n();
|
const { language, t } = useI18n();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Company | null>(null);
|
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 [recruiterName, setRecruiterName] = useState("");
|
||||||
const [recruiterEmail, setRecruiterEmail] = useState("");
|
const [recruiterEmail, setRecruiterEmail] = useState("");
|
||||||
@@ -82,6 +88,18 @@ export default function CompaniesTable() {
|
|||||||
setEditOpen(true);
|
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 canSave = useMemo(() => !!editing?.id, [editing]);
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
@@ -139,6 +157,9 @@ export default function CompaniesTable() {
|
|||||||
<IconButton size="small" aria-label={`${t("jobTableEdit")}: ${c.name}`} onClick={() => openEdit(c)}>
|
<IconButton size="small" aria-label={`${t("jobTableEdit")}: ${c.name}`} onClick={() => openEdit(c)}>
|
||||||
<EditOutlinedIcon fontSize="small" />
|
<EditOutlinedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton size="small" aria-label={`${t("companiesRelationship")}: ${c.name}`} onClick={() => void openRelationship(c)}>
|
||||||
|
<PeopleAltOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
|
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
|
||||||
@@ -183,6 +204,9 @@ export default function CompaniesTable() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""}</TableCell>
|
<TableCell>{c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""}</TableCell>
|
||||||
<TableCell align="right">
|
<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)}>
|
<IconButton size="small" aria-label={`${t("jobTableEdit")}: ${c.name}`} onClick={() => openEdit(c)}>
|
||||||
<EditOutlinedIcon fontSize="small" />
|
<EditOutlinedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -270,6 +294,52 @@ export default function CompaniesTable() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</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>
|
</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 }>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1431,6 +1431,14 @@ export const translations = {
|
|||||||
companiesUpdateFailed: "Failed to update company.",
|
companiesUpdateFailed: "Failed to update company.",
|
||||||
companiesLoadFailed: "Unable to load companies",
|
companiesLoadFailed: "Unable to load companies",
|
||||||
companiesLoadFailedBody: "The companies list is unavailable right now. Try again when the API is reachable.",
|
companiesLoadFailedBody: "The companies list is unavailable right now. Try again when the API is reachable.",
|
||||||
|
companiesRelationship: "Relationship history",
|
||||||
|
companiesRelationshipLoadFailed: "Could not load the relationship history.",
|
||||||
|
companiesApplications: "Applications at this company",
|
||||||
|
companiesNoApplications: "No active applications are linked to this company.",
|
||||||
|
companiesMessageCount: "{count} messages",
|
||||||
|
companiesRecentContact: "Recent contact",
|
||||||
|
companiesNoContact: "No correspondence has been recorded for this company.",
|
||||||
|
companiesContact: "Contact",
|
||||||
adminUsersTitle: "Users",
|
adminUsersTitle: "Users",
|
||||||
adminUsersSubtitle: "Admin-only user management.",
|
adminUsersSubtitle: "Admin-only user management.",
|
||||||
adminUsersCreateUser: "Create user",
|
adminUsersCreateUser: "Create user",
|
||||||
@@ -3812,6 +3820,14 @@ export const translations = {
|
|||||||
companiesUpdateFailed: "Kunne ikke oppdatere selskap.",
|
companiesUpdateFailed: "Kunne ikke oppdatere selskap.",
|
||||||
companiesLoadFailed: "Kunne ikke laste selskaper",
|
companiesLoadFailed: "Kunne ikke laste selskaper",
|
||||||
companiesLoadFailedBody: "Selskapslisten er utilgjengelig akkurat nå. Prøv igjen når API-et er tilgjengelig.",
|
companiesLoadFailedBody: "Selskapslisten er utilgjengelig akkurat nå. Prøv igjen når API-et er tilgjengelig.",
|
||||||
|
companiesRelationship: "Relasjonshistorikk",
|
||||||
|
companiesRelationshipLoadFailed: "Kunne ikke laste relasjonshistorikken.",
|
||||||
|
companiesApplications: "Søknader hos dette selskapet",
|
||||||
|
companiesNoApplications: "Ingen aktive søknader er knyttet til dette selskapet.",
|
||||||
|
companiesMessageCount: "{count} meldinger",
|
||||||
|
companiesRecentContact: "Nylig kontakt",
|
||||||
|
companiesNoContact: "Ingen korrespondanse er registrert for dette selskapet.",
|
||||||
|
companiesContact: "Kontakt",
|
||||||
adminUsersTitle: "Brukere",
|
adminUsersTitle: "Brukere",
|
||||||
adminUsersSubtitle: "Brukeradministrasjon kun for administratorer.",
|
adminUsersSubtitle: "Brukeradministrasjon kun for administratorer.",
|
||||||
adminUsersCreateUser: "Opprett bruker",
|
adminUsersCreateUser: "Opprett bruker",
|
||||||
|
|||||||
Reference in New Issue
Block a user