adding gui for the application
This commit is contained in:
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user