adding gui for the application

This commit is contained in:
2026-09-18 11:11:59 -04:00
parent ff7aa065e3
commit 256e87c8c8
47 changed files with 4398 additions and 12055 deletions
+228
View File
@@ -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}
</>
);
}