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
+126
View File
@@ -0,0 +1,126 @@
.primaryButton,
.secondaryButton {
border: 0;
border-radius: 999px;
padding: 12px 18px;
cursor: pointer;
font-weight: 700;
white-space: nowrap;
}
.primaryButton {
background: #2563eb;
color: #ffffff;
}
.secondaryButton {
background: #ffffff;
color: #172033;
box-shadow: inset 0 0 0 1px #d9e1ec;
}
.companyGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.companyCard {
padding: 22px;
display: grid;
gap: 18px;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 20px 50px rgba(17, 24, 39, 0.08);
h3,
p {
margin: 0;
}
p {
color: #687487;
}
}
.companyHeader {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
}
.deleteButton {
width: 38px;
height: 38px;
border: 0;
border-radius: 999px;
display: inline-grid;
place-items: center;
flex: 0 0 auto;
background: #fee2e2;
color: #b91c1c;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: 0.55;
}
svg {
width: 18px;
height: 18px;
fill: currentColor;
}
}
.employeeList {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pill {
border-radius: 999px;
padding: 7px 11px;
background: #eef4ff;
color: #1d4ed8;
font-size: 0.84rem;
font-weight: 700;
}
.form {
display: grid;
gap: 16px;
label {
display: grid;
gap: 8px;
color: #293548;
font-weight: 700;
}
input,
select {
border: 1px solid #d9e1ec;
border-radius: 14px;
padding: 12px 14px;
color: #172033;
font: inherit;
}
}
.error {
color: #b91c1c;
font-weight: 700;
}
.muted {
color: #687487;
}
@media (max-width: 980px) {
.companyGrid {
grid-template-columns: 1fr;
}
}
+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}
</>
);
}
+38
View File
@@ -0,0 +1,38 @@
.statsGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.card {
padding: 24px;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 20px 50px rgba(17, 24, 39, 0.08);
span {
display: block;
color: #6b7280;
margin-bottom: 12px;
}
strong {
font-size: 2.25rem;
letter-spacing: -0.04em;
}
}
.error {
color: #b91c1c;
font-weight: 700;
}
.muted {
color: #687487;
}
@media (max-width: 980px) {
.statsGrid {
grid-template-columns: 1fr;
}
}
+92
View File
@@ -0,0 +1,92 @@
import { useEffect, useState } from "react";
import { getCompanies } from "@api/companiesApi";
import { getWeekSchedule } from "@api/scheduleApi";
import { getUsers } from "@api/usersApi";
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 } from "@/types/users";
import styles from "./HomePage.module.scss";
type HomePageProps = {
backendPreference: BackendPreference;
};
export function HomePage({ backendPreference }: HomePageProps) {
const [companies, setCompanies] = useState<Company[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [users, setUsers] = useState<Employee[]>([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let isMounted = true;
async function loadDashboard() {
setError("");
setIsLoading(true);
try {
const [companiesResponse, usersResponse, scheduleResponse] = await Promise.all([
getCompanies(backendPreference),
getUsers(backendPreference),
getWeekSchedule(backendPreference),
]);
if (isMounted) {
setCompanies(companiesResponse);
setUsers(usersResponse);
setSchedules(scheduleResponse.schedules);
}
} catch (loadError) {
if (isMounted) {
setError(
loadError instanceof Error ? loadError.message : "Unable to load dashboard data",
);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
}
void loadDashboard();
return () => {
isMounted = false;
};
}, [backendPreference]);
const workingToday = new Set(
schedules
.filter((schedule) => schedule.enter_time && !schedule.out_time)
.map((schedule) => schedule.user_id),
).size;
return (
<>
<PageHeader
title="Employee Manager"
description="Track employee attendance, company teams, and weekly schedules from one dashboard."
/>
{error ? <p className={styles.error}>{error}</p> : null}
{isLoading ? <p className={styles.muted}>Loading dashboard...</p> : null}
<section className={styles.statsGrid} aria-label="Dashboard overview">
<article className={styles.card}>
<span>Total users</span>
<strong>{users.length}</strong>
</article>
<article className={styles.card}>
<span>Working today</span>
<strong>{workingToday}</strong>
</article>
<article className={styles.card}>
<span>Companies</span>
<strong>{companies.length}</strong>
</article>
</section>
</>
);
}
+56
View File
@@ -0,0 +1,56 @@
.calendar {
display: grid;
grid-template-columns: repeat(7, minmax(130px, 1fr));
gap: 12px;
}
.dayCard {
min-height: 210px;
padding: 16px;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 14px 35px rgba(17, 24, 39, 0.07);
h3 {
margin: 0 0 14px;
font-size: 1rem;
}
}
.shiftList {
display: grid;
gap: 10px;
}
.shift {
border-radius: 14px;
padding: 10px;
background: #f3f6fb;
strong,
span {
display: block;
}
span {
color: #687487;
font-size: 0.86rem;
margin-top: 4px;
}
}
.error {
color: #b91c1c;
font-weight: 700;
}
.muted {
color: #687487;
margin: 0;
}
@media (max-width: 980px) {
.calendar {
overflow-x: auto;
}
}
+131
View File
@@ -0,0 +1,131 @@
import { useEffect, useMemo, useState } from "react";
import { getWeekSchedule } from "@api/scheduleApi";
import { getUsers } from "@api/usersApi";
import { PageHeader } from "@components/PageHeader";
import type { BackendPreference } from "@/types/backend";
import type { Schedule } from "@/types/schedule";
import type { DayOfWeek, Employee } from "@/types/users";
import styles from "./SchedulePage.module.scss";
type SchedulePageProps = {
backendPreference: BackendPreference;
};
const weekDays: DayOfWeek[] = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
function formatDay(day: DayOfWeek): string {
return day.charAt(0).toUpperCase() + day.slice(1);
}
function formatTime(value: string | null): string {
if (!value) {
return "Not registered";
}
return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
export function SchedulePage({ backendPreference }: SchedulePageProps) {
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [users, setUsers] = useState<Employee[]>([]);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(true);
const schedulesByDay = useMemo(() => {
return schedules.reduce<Record<DayOfWeek, Schedule[]>>(
(accumulator, schedule) => {
accumulator[schedule.day_of_week] = [...accumulator[schedule.day_of_week], schedule];
return accumulator;
},
{
sunday: [],
monday: [],
tuesday: [],
wednesday: [],
thursday: [],
friday: [],
saturday: [],
},
);
}, [schedules]);
useEffect(() => {
let isMounted = true;
async function loadSchedule() {
setError("");
setIsLoading(true);
try {
const [scheduleResponse, usersResponse] = await Promise.all([
getWeekSchedule(backendPreference),
getUsers(backendPreference),
]);
if (isMounted) {
setSchedules(scheduleResponse.schedules);
setUsers(usersResponse);
}
} catch (loadError) {
if (isMounted) {
setError(loadError instanceof Error ? loadError.message : "Unable to load schedule");
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
}
void loadSchedule();
return () => {
isMounted = false;
};
}, [backendPreference]);
return (
<>
<PageHeader
title="Schedule"
description="Weekly scheduler calendar starts on Sunday and ends on Saturday so you can see who is working each day."
/>
{error ? <p className={styles.error}>{error}</p> : null}
{isLoading ? <p className={styles.muted}>Loading schedule...</p> : null}
<section className={styles.calendar} aria-label="Weekly schedule from Sunday to Saturday">
{weekDays.map((day) => (
<article className={styles.dayCard} key={day}>
<h3>{formatDay(day)}</h3>
<div className={styles.shiftList}>
{schedulesByDay[day].length > 0 ? (
schedulesByDay[day].map((schedule) => {
const user = users.find((item) => item.id === schedule.user_id);
return (
<div className={styles.shift} key={schedule.id}>
<strong>
{user ? `${user.name} ${user.last_name}` : `User #${schedule.user_id}`}
</strong>
<span>
{formatTime(schedule.enter_time)} - {formatTime(schedule.out_time)}
</span>
</div>
);
})
) : (
<p className={styles.muted}>No shifts scheduled</p>
)}
</div>
</article>
))}
</section>
</>
);
}
+121
View File
@@ -0,0 +1,121 @@
.button {
border: 0;
border-radius: 999px;
padding: 12px 18px;
cursor: pointer;
font-weight: 700;
white-space: nowrap;
}
.primaryButton {
composes: button;
background: #2563eb;
color: #ffffff;
}
.secondaryButton {
composes: button;
background: #ffffff;
color: #172033;
box-shadow: inset 0 0 0 1px #d9e1ec;
}
.tableCard {
overflow: hidden;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 20px 50px rgba(17, 24, 39, 0.08);
}
.tableScroll {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
th,
td {
padding: 16px 18px;
text-align: left;
border-bottom: 1px solid #edf1f6;
}
th {
color: #6b7280;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
background: #f8fafc;
}
td {
color: #293548;
}
tbody tr {
cursor: pointer;
}
}
.selectedRow {
background: #eef4ff;
}
.status {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 6px 10px;
font-size: 0.82rem;
font-weight: 700;
color: #047857;
background: #d1fae5;
}
.working {
color: #047857;
background: #d1fae5;
}
.worked {
color: #1d4ed8;
background: #dbeafe;
}
.off-duty {
color: #b45309;
background: #fef3c7;
}
.form {
display: grid;
gap: 16px;
label {
display: grid;
gap: 8px;
color: #293548;
font-weight: 700;
}
input,
select {
border: 1px solid #d9e1ec;
border-radius: 14px;
padding: 12px 14px;
color: #172033;
font: inherit;
}
}
.error {
color: #b91c1c;
font-weight: 700;
}
.muted {
color: #687487;
}
+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}
</>
);
}