adding gui for the application
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import {
|
||||
assignEmployeeToCompany,
|
||||
createCompany,
|
||||
deleteCompany,
|
||||
getCompanies,
|
||||
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 styles from "./CompaniesPage.module.scss";
|
||||
|
||||
type CompaniesPageProps = {
|
||||
backendPreference: BackendPreference;
|
||||
};
|
||||
|
||||
type EmployeeNamesByCompany = Record<number, string[]>;
|
||||
|
||||
export function CompaniesPage({ backendPreference }: CompaniesPageProps) {
|
||||
const [companies, setCompanies] = useState<Company[]>([]);
|
||||
const [employeeNamesByCompany, setEmployeeNamesByCompany] = useState<EmployeeNamesByCompany>({});
|
||||
const [users, setUsers] = useState<Employee[]>([]);
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
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 loadCompanies = useCallback(async () => {
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const [companiesResponse, usersResponse] = await Promise.all([
|
||||
getCompanies(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;
|
||||
}),
|
||||
);
|
||||
|
||||
setCompanies(companiesResponse);
|
||||
setUsers(usersResponse);
|
||||
setEmployeeNamesByCompany(Object.fromEntries(employeeNameEntries));
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : "Unable to load companies");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backendPreference]);
|
||||
|
||||
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(company: Company) {
|
||||
const shouldDelete = window.confirm(`Delete ${company.name}?`);
|
||||
|
||||
if (!shouldDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingCompanyId(company.id);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
await deleteCompany(backendPreference, company.id);
|
||||
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={() => void handleDeleteCompany(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 ? (
|
||||
<Modal
|
||||
title={`Add users to ${selectedCompany.name}`}
|
||||
onClose={() => setSelectedCompany(null)}
|
||||
>
|
||||
<form className={styles.form} onSubmit={handleAssignEmployee}>
|
||||
<label>
|
||||
Existing user
|
||||
<select
|
||||
value={selectedEmployeeId}
|
||||
onChange={(event) => setSelectedEmployeeId(event.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Select a user</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name} {user.last_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button className={styles.primaryButton} disabled={isSaving} type="submit">
|
||||
{isSaving ? "Saving..." : "Assign user"}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user