adding scheduler

This commit is contained in:
2026-09-18 12:27:51 -04:00
parent 256e87c8c8
commit f4f3f76d6c
7 changed files with 273 additions and 35 deletions
+30 -1
View File
@@ -1,5 +1,6 @@
.primaryButton,
.secondaryButton {
.secondaryButton,
.dangerButton {
border: 0;
border-radius: 999px;
padding: 12px 18px;
@@ -19,6 +20,16 @@
box-shadow: inset 0 0 0 1px #d9e1ec;
}
.dangerButton {
background: #dc2626;
color: #ffffff;
&:disabled {
cursor: not-allowed;
opacity: 0.65;
}
}
.companyGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -110,6 +121,24 @@
}
}
.confirmation {
display: grid;
gap: 18px;
p {
margin: 0;
color: #293548;
line-height: 1.6;
}
}
.confirmationActions {
display: flex;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.error {
color: #b91c1c;
font-weight: 700;
+75 -31
View File
@@ -26,6 +26,7 @@ export function CompaniesPage({ backendPreference }: CompaniesPageProps) {
const [employeeNamesByCompany, setEmployeeNamesByCompany] = useState<EmployeeNamesByCompany>({});
const [users, setUsers] = useState<Employee[]>([]);
const [companyName, setCompanyName] = useState("");
const [companyToDelete, setCompanyToDelete] = useState<Company | null>(null);
const [selectedCompany, setSelectedCompany] = useState<Company | null>(null);
const [selectedEmployeeId, setSelectedEmployeeId] = useState("");
const [error, setError] = useState("");
@@ -107,18 +108,17 @@ export function CompaniesPage({ backendPreference }: CompaniesPageProps) {
}
}
async function handleDeleteCompany(company: Company) {
const shouldDelete = window.confirm(`Delete ${company.name}?`);
if (!shouldDelete) {
async function handleDeleteCompany() {
if (!companyToDelete) {
return;
}
setDeletingCompanyId(company.id);
setDeletingCompanyId(companyToDelete.id);
setError("");
try {
await deleteCompany(backendPreference, company.id);
await deleteCompany(backendPreference, companyToDelete.id);
setCompanyToDelete(null);
await loadCompanies();
} catch (deleteError) {
setError(deleteError instanceof Error ? deleteError.message : "Unable to delete company");
@@ -151,7 +151,7 @@ export function CompaniesPage({ backendPreference }: CompaniesPageProps) {
<button
className={styles.deleteButton}
disabled={deletingCompanyId === company.id}
onClick={() => void handleDeleteCompany(company)}
onClick={() => setCompanyToDelete(company)}
type="button"
aria-label={`Delete ${company.name}`}
>
@@ -196,31 +196,75 @@ export function CompaniesPage({ backendPreference }: CompaniesPageProps) {
</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
{selectedCompany
? (() => {
const assignedNames = new Set(employeeNamesByCompany[selectedCompany.id] ?? []);
const availableUsers = users.filter(
(user) => !assignedNames.has(`${user.name} ${user.last_name}`),
);
return (
<Modal
title={`Add users to ${selectedCompany.name}`}
onClose={() => setSelectedCompany(null)}
>
<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>
<form className={styles.form} onSubmit={handleAssignEmployee}>
{availableUsers.length === 0 ? (
<p className={styles.muted}>All users are in a company</p>
) : (
<label>
Existing user
<select
value={selectedEmployeeId}
onChange={(event) => setSelectedEmployeeId(event.target.value)}
required
>
<option value="">Select a user</option>
{availableUsers.map((user) => (
<option key={user.id} value={user.id}>
{user.name} {user.last_name}
</option>
))}
</select>
</label>
)}
<button
className={styles.primaryButton}
disabled={isSaving || availableUsers.length === 0}
type="submit"
>
{isSaving ? "Saving..." : "Assign user"}
</button>
</form>
</Modal>
);
})()
: null}
{companyToDelete ? (
<Modal title="Delete company" onClose={() => setCompanyToDelete(null)}>
<div className={styles.confirmation}>
<p>
Delete <strong>{companyToDelete.name}</strong>? This action cannot be undone.
</p>
<div className={styles.confirmationActions}>
<button
className={styles.secondaryButton}
onClick={() => setCompanyToDelete(null)}
type="button"
>
Cancel
</button>
<button
className={styles.dangerButton}
disabled={deletingCompanyId === companyToDelete.id}
onClick={() => void handleDeleteCompany()}
type="button"
>
{deletingCompanyId === companyToDelete.id ? "Deleting..." : "Delete company"}
</button>
</div>
</div>
</Modal>
) : null}
</>
+23
View File
@@ -20,6 +20,29 @@
box-shadow: inset 0 0 0 1px #d9e1ec;
}
.actionGroup {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.actionButton {
border: 0;
border-radius: 999px;
padding: 8px 11px;
background: #eef4ff;
color: #1d4ed8;
cursor: pointer;
font-size: 0.82rem;
font-weight: 700;
white-space: nowrap;
&:disabled {
cursor: not-allowed;
opacity: 0.45;
}
}
.tableCard {
overflow: hidden;
background: #ffffff;
+136 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import type { FormEvent } from "react";
import { getCompanies } from "@api/companiesApi";
import { getWeekSchedule } from "@api/scheduleApi";
import { clockIn, clockOut, getWeekSchedule } from "@api/scheduleApi";
import { createUser, getUsers, updateUserPreference } from "@api/usersApi";
import { Modal } from "@components/Modal";
import { PageHeader } from "@components/PageHeader";
@@ -66,6 +66,9 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
const [error, setError] = useState("");
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
const [isPreferenceOpen, setIsPreferenceOpen] = useState(false);
const [checkingInUser, setCheckingInUser] = useState<Employee | null>(null);
const [checkInDateTime, setCheckInDateTime] = useState("");
const [savingActionUserId, setSavingActionUserId] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
@@ -153,6 +156,69 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
}
}
function getDayOfWeekFromDateTime(value: string): import("@/types/users").DayOfWeek {
const days: import("@/types/users").DayOfWeek[] = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
return days[new Date(value).getDay()];
}
function openCheckInModal(user: Employee) {
const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16);
setCheckingInUser(user);
setCheckInDateTime(local);
}
async function handleClockIn(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!checkingInUser || !checkInDateTime) {
return;
}
setError("");
setSavingActionUserId(checkingInUser.id);
try {
await clockIn(backendPreference, checkingInUser.id, {
day_of_week: getDayOfWeekFromDateTime(checkInDateTime),
enter_time: new Date(checkInDateTime).toISOString(),
});
setCheckingInUser(null);
await loadUsersPage();
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "Unable to check in user");
} finally {
setSavingActionUserId(null);
}
}
async function handleClockOut(userId: number, activeSchedule: Schedule) {
setError("");
setSavingActionUserId(userId);
try {
await clockOut(backendPreference, userId, {
day_of_week: activeSchedule.day_of_week,
out_time: new Date().toISOString(),
});
await loadUsersPage();
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "Unable to check out user");
} finally {
setSavingActionUserId(null);
}
}
return (
<>
<PageHeader
@@ -191,12 +257,16 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
<th>Check in</th>
<th>Check out</th>
<th>Days worked</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map((user) => {
const userSchedules = schedulesByUser[user.id] ?? [];
const status = getUserStatus(userSchedules);
const activeSchedule = userSchedules.find(
(schedule) => schedule.enter_time && !schedule.out_time,
);
const latestSchedule = userSchedules.at(-1);
const company = companies.find((item) => item.id === user.company_id);
const daysWorked = new Set(
@@ -210,11 +280,15 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
className={selectedUserId === user.id ? styles.selectedRow : ""}
key={user.id}
onClick={() => setSelectedUserId(user.id)}
title="Double click to edit user information"
>
<td>
{user.name} {user.last_name}
</td>
<td>{company?.name ?? `Company #${user.company_id}`}</td>
<td>
{company?.name ??
(user.company_id ? `Company #${user.company_id}` : "Unassigned")}
</td>
<td>{user.preference}</td>
<td>
<span className={`${styles.status} ${styles[status]}`}>
@@ -224,6 +298,35 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
<td>{formatDateTime(latestSchedule?.enter_time ?? null)}</td>
<td>{formatDateTime(latestSchedule?.out_time ?? null)}</td>
<td>{daysWorked}</td>
<td>
<div className={styles.actionGroup}>
<button
className={styles.actionButton}
disabled={Boolean(activeSchedule) || savingActionUserId === user.id}
onClick={(event) => {
event.stopPropagation();
openCheckInModal(user);
}}
type="button"
>
Check in
</button>
<button
className={styles.actionButton}
disabled={!activeSchedule || savingActionUserId === user.id}
onClick={(event) => {
event.stopPropagation();
if (activeSchedule) {
void handleClockOut(user.id, activeSchedule);
}
}}
type="button"
>
Check out
</button>
</div>
</td>
</tr>
);
})}
@@ -314,6 +417,37 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
</form>
</Modal>
) : null}
{checkingInUser ? (
<Modal
title={`Check in ${checkingInUser.name} ${checkingInUser.last_name}`}
onClose={() => setCheckingInUser(null)}
>
<form className={styles.form} onSubmit={handleClockIn}>
<label>
Date and time
<input
type="datetime-local"
value={checkInDateTime}
onChange={(event) => setCheckInDateTime(event.target.value)}
required
/>
</label>
<p className={styles.muted}>
Day of week will be derived as{" "}
{checkInDateTime ? getDayOfWeekFromDateTime(checkInDateTime) : "--"} from the selected
date.
</p>
<button
className={styles.primaryButton}
disabled={savingActionUserId === checkingInUser.id}
type="submit"
>
{savingActionUserId === checkingInUser.id ? "Saving..." : "Check in"}
</button>
</form>
</Modal>
) : null}
</>
);
}