updating the layout based on the endpoints
This commit is contained in:
+176
-18
@@ -18,20 +18,51 @@ type UsersPageProps = {
|
||||
type UserStatus = "off-duty" | "working" | "worked";
|
||||
|
||||
const workTimeOptions: WorkTime[] = ["morning", "afternoon", "evening"];
|
||||
const statusOptions: UserStatus[] = ["off-duty", "working", "worked"];
|
||||
const usersPerPage = 10;
|
||||
|
||||
function getUserStatus(schedules: Schedule[]): UserStatus {
|
||||
const hasCheckIn = schedules.some((schedule) => schedule.enter_time);
|
||||
const hasActiveShift = schedules.some((schedule) => schedule.enter_time && !schedule.out_time);
|
||||
function isSameLocalDate(firstDate: Date, secondDate: Date): boolean {
|
||||
return (
|
||||
firstDate.getFullYear() === secondDate.getFullYear() &&
|
||||
firstDate.getMonth() === secondDate.getMonth() &&
|
||||
firstDate.getDate() === secondDate.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
if (hasActiveShift) {
|
||||
return "working";
|
||||
function getTodaySchedules(schedules: Schedule[], today: Date): Schedule[] {
|
||||
return schedules.filter((schedule) => {
|
||||
if (!schedule.enter_time) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const enterDate = new Date(schedule.enter_time);
|
||||
|
||||
return !Number.isNaN(enterDate.getTime()) && isSameLocalDate(enterDate, today);
|
||||
});
|
||||
}
|
||||
|
||||
function getLatestSchedule(schedules: Schedule[]): Schedule | undefined {
|
||||
return [...schedules].sort((firstSchedule, secondSchedule) => {
|
||||
const firstTime = firstSchedule.enter_time ? new Date(firstSchedule.enter_time).getTime() : 0;
|
||||
const secondTime = secondSchedule.enter_time ? new Date(secondSchedule.enter_time).getTime() : 0;
|
||||
|
||||
return firstTime - secondTime;
|
||||
}).at(-1);
|
||||
}
|
||||
|
||||
function getUserStatus(schedules: Schedule[], today: Date): UserStatus {
|
||||
const todaySchedules = getTodaySchedules(schedules, today);
|
||||
const latestSchedule = getLatestSchedule(todaySchedules);
|
||||
|
||||
if (!latestSchedule?.enter_time) {
|
||||
return "off-duty";
|
||||
}
|
||||
|
||||
if (hasCheckIn) {
|
||||
if (latestSchedule.out_time) {
|
||||
return "worked";
|
||||
}
|
||||
|
||||
return "off-duty";
|
||||
return "working";
|
||||
}
|
||||
|
||||
function getStatusLabel(status: UserStatus): string {
|
||||
@@ -54,6 +85,30 @@ function formatDateTime(value: string | null): string {
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
function getNameScheduleKey(name: string, lastName: string): string {
|
||||
return `name:${name.trim().toLowerCase()}:${lastName.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
function getEmployeeScheduleKeys(user: Employee): string[] {
|
||||
return [`id:${user.id}`, getNameScheduleKey(user.name, user.last_name)];
|
||||
}
|
||||
|
||||
function getScheduleUserKey(schedule: Schedule): string | null {
|
||||
if (typeof schedule.user_id === "number") {
|
||||
return `id:${schedule.user_id}`;
|
||||
}
|
||||
|
||||
if (schedule.user) {
|
||||
return getNameScheduleKey(schedule.user.name, schedule.user.last_name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSchedulesForUser(user: Employee, schedulesByUser: Record<string, Schedule[]>): Schedule[] {
|
||||
return getEmployeeScheduleKeys(user).flatMap((key) => schedulesByUser[key] ?? []);
|
||||
}
|
||||
|
||||
export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
const [companies, setCompanies] = useState<Company[]>([]);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
@@ -71,20 +126,42 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
const [savingActionUserId, setSavingActionUserId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedCompanyId, setSelectedCompanyId] = useState("");
|
||||
const [selectedStatus, setSelectedStatus] = useState<UserStatus | "">("");
|
||||
|
||||
const selectedUser = users.find((user) => user.id === selectedUserId) ?? null;
|
||||
const today = new Date();
|
||||
|
||||
const schedulesByUser = useMemo(() => {
|
||||
return schedules.reduce<Record<number, Schedule[]>>((accumulator, schedule) => {
|
||||
if (typeof schedule.user_id !== "number") {
|
||||
return schedules.reduce<Record<string, Schedule[]>>((accumulator, schedule) => {
|
||||
const userKey = getScheduleUserKey(schedule);
|
||||
|
||||
if (!userKey) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
accumulator[schedule.user_id] = [...(accumulator[schedule.user_id] ?? []), schedule];
|
||||
accumulator[userKey] = [...(accumulator[userKey] ?? []), schedule];
|
||||
return accumulator;
|
||||
}, {});
|
||||
}, [schedules]);
|
||||
|
||||
const filteredUsers = users.filter((user) => {
|
||||
if (selectedCompanyId && user.company_id !== Number(selectedCompanyId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedStatus && getUserStatus(getSchedulesForUser(user, schedulesByUser), today) !== selectedStatus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filteredUsers.length / usersPerPage));
|
||||
const pageStartIndex = (currentPage - 1) * usersPerPage;
|
||||
const paginatedUsers = filteredUsers.slice(pageStartIndex, pageStartIndex + usersPerPage);
|
||||
const pageEndIndex = pageStartIndex + paginatedUsers.length;
|
||||
|
||||
const loadUsersPage = useCallback(async () => {
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
@@ -110,6 +187,16 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
void loadUsersPage();
|
||||
}, [loadUsersPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > totalPages) {
|
||||
setCurrentPage(totalPages);
|
||||
}
|
||||
}, [currentPage, totalPages]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [selectedCompanyId, selectedStatus]);
|
||||
|
||||
function resetUserForm() {
|
||||
setCompanyId("");
|
||||
setFirstName("");
|
||||
@@ -173,6 +260,20 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
return days[new Date(value).getDay()];
|
||||
}
|
||||
|
||||
function getShiftFromDateTime(value: string): WorkTime {
|
||||
const hour = new Date(value).getHours();
|
||||
|
||||
if (hour < 12) {
|
||||
return "morning";
|
||||
}
|
||||
|
||||
if (hour < 19) {
|
||||
return "evening";
|
||||
}
|
||||
|
||||
return "afternoon";
|
||||
}
|
||||
|
||||
function openCheckInModal(user: Employee) {
|
||||
const now = new Date();
|
||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
|
||||
@@ -196,6 +297,7 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
await clockIn(backendPreference, checkingInUser.id, {
|
||||
day_of_week: getDayOfWeekFromDateTime(checkInDateTime),
|
||||
enter_time: new Date(checkInDateTime).toISOString(),
|
||||
shift: getShiftFromDateTime(checkInDateTime),
|
||||
});
|
||||
setCheckingInUser(null);
|
||||
await loadUsersPage();
|
||||
@@ -249,6 +351,34 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
/>
|
||||
{error ? <p className={styles.error}>{error}</p> : null}
|
||||
{isLoading ? <p className={styles.muted}>Loading users...</p> : null}
|
||||
<div className={styles.usersToolbar}>
|
||||
<label className={styles.companyFilter}>
|
||||
<select
|
||||
value={selectedCompanyId}
|
||||
onChange={(event) => setSelectedCompanyId(event.target.value)}
|
||||
>
|
||||
<option value="">All companies</option>
|
||||
{companies.map((company) => (
|
||||
<option key={company.id} value={company.id}>
|
||||
{company.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className={styles.companyFilter}>
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(event) => setSelectedStatus(event.target.value as UserStatus | "")}
|
||||
>
|
||||
<option value="">All statuses</option>
|
||||
{statusOptions.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getStatusLabel(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<section className={styles.tableCard} aria-label="Users table">
|
||||
<div className={styles.tableScroll}>
|
||||
<table className={styles.table}>
|
||||
@@ -265,13 +395,14 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
</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);
|
||||
{paginatedUsers.map((user) => {
|
||||
const userSchedules = getSchedulesForUser(user, schedulesByUser);
|
||||
const todaySchedules = getTodaySchedules(userSchedules, today);
|
||||
const status = getUserStatus(userSchedules, today);
|
||||
const latestSchedule = getLatestSchedule(todaySchedules);
|
||||
const activeSchedule = latestSchedule?.enter_time && !latestSchedule.out_time
|
||||
? latestSchedule
|
||||
: undefined;
|
||||
const company = companies.find((item) => item.id === user.company_id);
|
||||
const daysWorked = new Set(
|
||||
userSchedules
|
||||
@@ -306,7 +437,7 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
<div className={styles.actionGroup}>
|
||||
<button
|
||||
className={styles.actionButton}
|
||||
disabled={Boolean(activeSchedule) || savingActionUserId === user.id}
|
||||
disabled={status !== "off-duty" || savingActionUserId === user.id}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openCheckInModal(user);
|
||||
@@ -337,6 +468,33 @@ export function UsersPage({ backendPreference }: UsersPageProps) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.pagination}>
|
||||
<span>
|
||||
Showing {filteredUsers.length === 0 ? 0 : pageStartIndex + 1}-{pageEndIndex} of{" "}
|
||||
{filteredUsers.length} users
|
||||
</span>
|
||||
<div className={styles.paginationActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
|
||||
type="button"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{isAddUserOpen ? (
|
||||
|
||||
Reference in New Issue
Block a user