273 lines
9.5 KiB
TypeScript
273 lines
9.5 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import type { FormEvent } from "react";
|
|
import {
|
|
assignEmployeeToCompany,
|
|
createCompany,
|
|
deleteCompany,
|
|
getCompanyEmployeeNames,
|
|
} from "@api/companiesApi";
|
|
import { getUsers } from "@api/usersApi";
|
|
import { Modal } from "@components/Modal";
|
|
import { PageHeader } from "@components/PageHeader";
|
|
import type { BackendPreference } from "@/types/backend";
|
|
import type { Company } from "@/types/companies";
|
|
import type { Employee } from "@/types/users";
|
|
import { useCompaniesStore } from "../store/companiesStore";
|
|
import styles from "./CompaniesPage.module.scss";
|
|
|
|
type CompaniesPageProps = {
|
|
backendPreference: BackendPreference;
|
|
};
|
|
|
|
type EmployeeNamesByCompany = Record<number, string[]>;
|
|
|
|
export function CompaniesPage({ backendPreference }: CompaniesPageProps) {
|
|
const [employeeNamesByCompany, setEmployeeNamesByCompany] = useState<EmployeeNamesByCompany>({});
|
|
const [users, setUsers] = useState<Employee[]>([]);
|
|
const [companyName, setCompanyName] = useState("");
|
|
const [companyToDelete, setCompanyToDelete] = useState<Company | null>(null);
|
|
const [selectedCompany, setSelectedCompany] = useState<Company | null>(null);
|
|
const [selectedEmployeeId, setSelectedEmployeeId] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [isAddCompanyOpen, setIsAddCompanyOpen] = useState(false);
|
|
const [deletingCompanyId, setDeletingCompanyId] = useState<number | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const companies = useCompaniesStore((state) => state.companies);
|
|
const refreshCompanies = useCompaniesStore((state) => state.loadCompanies);
|
|
|
|
const loadCompanies = useCallback(async () => {
|
|
setError("");
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const [companiesResponse, usersResponse] = await Promise.all([
|
|
refreshCompanies(backendPreference),
|
|
getUsers(backendPreference),
|
|
]);
|
|
const employeeNameEntries = await Promise.all(
|
|
companiesResponse.map(async (company) => {
|
|
const response = await getCompanyEmployeeNames(backendPreference, company.name);
|
|
return [company.id, response.employees] as const;
|
|
}),
|
|
);
|
|
|
|
setUsers(usersResponse);
|
|
setEmployeeNamesByCompany(Object.fromEntries(employeeNameEntries));
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Unable to load companies");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [backendPreference, refreshCompanies]);
|
|
|
|
useEffect(() => {
|
|
void loadCompanies();
|
|
}, [loadCompanies]);
|
|
|
|
async function handleCreateCompany(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
setIsSaving(true);
|
|
setError("");
|
|
|
|
try {
|
|
await createCompany(backendPreference, { name: companyName });
|
|
setCompanyName("");
|
|
setIsAddCompanyOpen(false);
|
|
await loadCompanies();
|
|
} catch (saveError) {
|
|
setError(saveError instanceof Error ? saveError.message : "Unable to create company");
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleAssignEmployee(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
|
|
if (!selectedCompany || !selectedEmployeeId) {
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setError("");
|
|
|
|
try {
|
|
await assignEmployeeToCompany(
|
|
backendPreference,
|
|
selectedCompany.id,
|
|
Number(selectedEmployeeId),
|
|
);
|
|
setSelectedCompany(null);
|
|
setSelectedEmployeeId("");
|
|
await loadCompanies();
|
|
} catch (saveError) {
|
|
setError(saveError instanceof Error ? saveError.message : "Unable to assign employee");
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleDeleteCompany() {
|
|
if (!companyToDelete) {
|
|
return;
|
|
}
|
|
|
|
setDeletingCompanyId(companyToDelete.id);
|
|
setError("");
|
|
|
|
try {
|
|
await deleteCompany(backendPreference, companyToDelete.id);
|
|
setCompanyToDelete(null);
|
|
await loadCompanies();
|
|
} catch (deleteError) {
|
|
setError(deleteError instanceof Error ? deleteError.message : "Unable to delete company");
|
|
} finally {
|
|
setDeletingCompanyId(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title="Companies"
|
|
description="Create companies, view company names, and see the users assigned to each company."
|
|
actions={
|
|
<button className={styles.primaryButton} onClick={() => setIsAddCompanyOpen(true)}>
|
|
Add company
|
|
</button>
|
|
}
|
|
/>
|
|
{error ? <p className={styles.error}>{error}</p> : null}
|
|
{isLoading ? <p className={styles.muted}>Loading companies...</p> : null}
|
|
<section className={styles.companyGrid} aria-label="Company list">
|
|
{companies.map((company) => (
|
|
<article className={styles.companyCard} key={company.id}>
|
|
<div className={styles.companyHeader}>
|
|
<div>
|
|
<h3>{company.name}</h3>
|
|
<p>Created {new Date(company.date_created).toLocaleDateString()}</p>
|
|
</div>
|
|
<button
|
|
className={styles.deleteButton}
|
|
disabled={deletingCompanyId === company.id}
|
|
onClick={() => setCompanyToDelete(company)}
|
|
type="button"
|
|
aria-label={`Delete ${company.name}`}
|
|
>
|
|
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
|
|
<path d="M9 3h6l1 2h4v2H4V5h4l1-2Zm1 6h2v9h-2V9Zm4 0h2v9h-2V9ZM7 9h2v10h6V9h2v12H7V9Z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div className={styles.employeeList} aria-label={`${company.name} users`}>
|
|
{(employeeNamesByCompany[company.id] ?? []).length > 0 ? (
|
|
employeeNamesByCompany[company.id].map((user) => (
|
|
<span className={styles.pill} key={user}>
|
|
{user}
|
|
</span>
|
|
))
|
|
) : (
|
|
<span className={styles.muted}>No users assigned</span>
|
|
)}
|
|
</div>
|
|
<button className={styles.secondaryButton} onClick={() => setSelectedCompany(company)}>
|
|
Add users
|
|
</button>
|
|
</article>
|
|
))}
|
|
</section>
|
|
|
|
{isAddCompanyOpen ? (
|
|
<Modal title="Add company" onClose={() => setIsAddCompanyOpen(false)}>
|
|
<form className={styles.form} onSubmit={handleCreateCompany}>
|
|
<label>
|
|
Company name
|
|
<input
|
|
value={companyName}
|
|
onChange={(event) => setCompanyName(event.target.value)}
|
|
required
|
|
/>
|
|
</label>
|
|
<button className={styles.primaryButton} disabled={isSaving} type="submit">
|
|
{isSaving ? "Saving..." : "Create company"}
|
|
</button>
|
|
</form>
|
|
</Modal>
|
|
) : null}
|
|
|
|
{selectedCompany
|
|
? (() => {
|
|
const assignedNames = new Set(employeeNamesByCompany[selectedCompany.id] ?? []);
|
|
const availableUsers = users.filter(
|
|
(user) => !assignedNames.has(`${user.name} ${user.last_name}`),
|
|
);
|
|
|
|
return (
|
|
<Modal
|
|
title={`Add users to ${selectedCompany.name}`}
|
|
onClose={() => setSelectedCompany(null)}
|
|
>
|
|
<form className={styles.form} onSubmit={handleAssignEmployee}>
|
|
{availableUsers.length === 0 ? (
|
|
<p className={styles.muted}>All users are in a company</p>
|
|
) : (
|
|
<label>
|
|
Existing user
|
|
<select
|
|
value={selectedEmployeeId}
|
|
onChange={(event) => setSelectedEmployeeId(event.target.value)}
|
|
required
|
|
>
|
|
<option value="">Select a user</option>
|
|
{availableUsers.map((user) => (
|
|
<option key={user.id} value={user.id}>
|
|
{user.name} {user.last_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
<button
|
|
className={styles.primaryButton}
|
|
disabled={isSaving || availableUsers.length === 0}
|
|
type="submit"
|
|
>
|
|
{isSaving ? "Saving..." : "Assign user"}
|
|
</button>
|
|
</form>
|
|
</Modal>
|
|
);
|
|
})()
|
|
: null}
|
|
|
|
{companyToDelete ? (
|
|
<Modal title="Delete company" onClose={() => setCompanyToDelete(null)}>
|
|
<div className={styles.confirmation}>
|
|
<p>
|
|
Delete <strong>{companyToDelete.name}</strong>? This action cannot be undone.
|
|
</p>
|
|
<div className={styles.confirmationActions}>
|
|
<button
|
|
className={styles.secondaryButton}
|
|
onClick={() => setCompanyToDelete(null)}
|
|
type="button"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
className={styles.dangerButton}
|
|
disabled={deletingCompanyId === companyToDelete.id}
|
|
onClick={() => void handleDeleteCompany()}
|
|
type="button"
|
|
>
|
|
{deletingCompanyId === companyToDelete.id ? "Deleting..." : "Delete company"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|