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
+319
View File
@@ -0,0 +1,319 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import type { FormEvent } from "react";
import { getCompanies } from "@api/companiesApi";
import { getWeekSchedule } from "@api/scheduleApi";
import { createUser, getUsers, updateUserPreference } 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 { Schedule } from "@/types/schedule";
import type { Employee, WorkTime } from "@/types/users";
import styles from "./UsersPage.module.scss";
type UsersPageProps = {
backendPreference: BackendPreference;
};
type UserStatus = "off-duty" | "working" | "worked";
const workTimeOptions: WorkTime[] = ["morning", "afternoon", "evening"];
function getUserStatus(schedules: Schedule[]): UserStatus {
const hasCheckIn = schedules.some((schedule) => schedule.enter_time);
const hasActiveShift = schedules.some((schedule) => schedule.enter_time && !schedule.out_time);
if (hasActiveShift) {
return "working";
}
if (hasCheckIn) {
return "worked";
}
return "off-duty";
}
function getStatusLabel(status: UserStatus): string {
if (status === "working") {
return "Working";
}
if (status === "worked") {
return "Worked";
}
return "Off duty";
}
function formatDateTime(value: string | null): string {
if (!value) {
return "--";
}
return new Date(value).toLocaleString();
}
export function UsersPage({ backendPreference }: UsersPageProps) {
const [companies, setCompanies] = useState<Company[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
const [users, setUsers] = useState<Employee[]>([]);
const [companyId, setCompanyId] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [preference, setPreference] = useState<WorkTime>("morning");
const [error, setError] = useState("");
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
const [isPreferenceOpen, setIsPreferenceOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const selectedUser = users.find((user) => user.id === selectedUserId) ?? null;
const schedulesByUser = useMemo(() => {
return schedules.reduce<Record<number, Schedule[]>>((accumulator, schedule) => {
accumulator[schedule.user_id] = [...(accumulator[schedule.user_id] ?? []), schedule];
return accumulator;
}, {});
}, [schedules]);
const loadUsersPage = useCallback(async () => {
setError("");
setIsLoading(true);
try {
const [companiesResponse, usersResponse, scheduleResponse] = await Promise.all([
getCompanies(backendPreference),
getUsers(backendPreference),
getWeekSchedule(backendPreference),
]);
setCompanies(companiesResponse);
setUsers(usersResponse);
setSchedules(scheduleResponse.schedules);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Unable to load users");
} finally {
setIsLoading(false);
}
}, [backendPreference]);
useEffect(() => {
void loadUsersPage();
}, [loadUsersPage]);
function resetUserForm() {
setCompanyId("");
setFirstName("");
setLastName("");
setPreference("morning");
}
async function handleCreateUser(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setIsSaving(true);
try {
await createUser(backendPreference, {
company_id: Number(companyId),
name: firstName,
last_name: lastName,
preference,
});
resetUserForm();
setIsAddUserOpen(false);
await loadUsersPage();
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "Unable to add user");
} finally {
setIsSaving(false);
}
}
async function handleUpdatePreference(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!selectedUser) {
return;
}
setError("");
setIsSaving(true);
try {
await updateUserPreference(backendPreference, selectedUser.id, { preference });
setIsPreferenceOpen(false);
await loadUsersPage();
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "Unable to update preference");
} finally {
setIsSaving(false);
}
}
return (
<>
<PageHeader
title="Users"
description="Review employees, assigned companies, active work status, check-in times, and check-out times."
actions={
<>
<button className={styles.primaryButton} onClick={() => setIsAddUserOpen(true)}>
Add user
</button>
{selectedUser ? (
<button
className={styles.secondaryButton}
onClick={() => {
setPreference(selectedUser.preference);
setIsPreferenceOpen(true);
}}
>
Update preference
</button>
) : null}
</>
}
/>
{error ? <p className={styles.error}>{error}</p> : null}
{isLoading ? <p className={styles.muted}>Loading users...</p> : null}
<section className={styles.tableCard} aria-label="Users table">
<div className={styles.tableScroll}>
<table className={styles.table}>
<thead>
<tr>
<th>Name</th>
<th>Company</th>
<th>Preference</th>
<th>Status</th>
<th>Check in</th>
<th>Check out</th>
<th>Days worked</th>
</tr>
</thead>
<tbody>
{users.map((user) => {
const userSchedules = schedulesByUser[user.id] ?? [];
const status = getUserStatus(userSchedules);
const latestSchedule = userSchedules.at(-1);
const company = companies.find((item) => item.id === user.company_id);
const daysWorked = new Set(
userSchedules
.filter((schedule) => schedule.enter_time)
.map((schedule) => schedule.day_of_week),
).size;
return (
<tr
className={selectedUserId === user.id ? styles.selectedRow : ""}
key={user.id}
onClick={() => setSelectedUserId(user.id)}
>
<td>
{user.name} {user.last_name}
</td>
<td>{company?.name ?? `Company #${user.company_id}`}</td>
<td>{user.preference}</td>
<td>
<span className={`${styles.status} ${styles[status]}`}>
{getStatusLabel(status)}
</span>
</td>
<td>{formatDateTime(latestSchedule?.enter_time ?? null)}</td>
<td>{formatDateTime(latestSchedule?.out_time ?? null)}</td>
<td>{daysWorked}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
{isAddUserOpen ? (
<Modal
title="Add user"
onClose={() => {
setIsAddUserOpen(false);
resetUserForm();
}}
>
<form className={styles.form} onSubmit={handleCreateUser}>
<label>
Company
<select
value={companyId}
onChange={(event) => setCompanyId(event.target.value)}
required
>
<option value="">Select a company</option>
{companies.map((company) => (
<option key={company.id} value={company.id}>
{company.name}
</option>
))}
</select>
</label>
<label>
First name
<input
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
required
/>
</label>
<label>
Last name
<input
value={lastName}
onChange={(event) => setLastName(event.target.value)}
required
/>
</label>
<label>
Preference
<select
value={preference}
onChange={(event) => setPreference(event.target.value as WorkTime)}
>
{workTimeOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<button className={styles.primaryButton} disabled={isSaving} type="submit">
{isSaving ? "Saving..." : "Create user"}
</button>
</form>
</Modal>
) : null}
{isPreferenceOpen && selectedUser ? (
<Modal title="Update preference" onClose={() => setIsPreferenceOpen(false)}>
<form className={styles.form} onSubmit={handleUpdatePreference}>
<label>
Preference for {selectedUser.name} {selectedUser.last_name}
<select
value={preference}
onChange={(event) => setPreference(event.target.value as WorkTime)}
>
{workTimeOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<button className={styles.primaryButton} disabled={isSaving} type="submit">
{isSaving ? "Saving..." : "Save preference"}
</button>
</form>
</Modal>
) : null}
</>
);
}