Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}

.small {
flex: 1 1 0;
flex: 0.5 1 0;
}

.fluid {
Expand Down
159 changes: 147 additions & 12 deletions frontend/src/components/admin-search-bar/admin-search-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import clsx from "clsx";
import { useState } from "react";
import {
addDays,
addMonths,
addWeeks,
addYears,
format,
subDays,
subMonths,
subWeeks,
subYears,
} from "date-fns";
import { useState, useMemo } from "react";
import { Icon, Input, Dropdown, DropdownProps } from "semantic-ui-react";

import styles from "./admin-search-bar.module.scss";
Expand All @@ -8,6 +19,7 @@ export type Filters = {
title: string;
venue: string;
date: string;
status: string;
};

type Props = {
Expand All @@ -31,11 +43,35 @@ const venueOptions = [
{ key: "tr3", text: "Theme Room 3", value: "Theme Room 3" },
];

const statusOptions = [
{ key: "app", text: "Approved", value: "Approved" },
{ key: "pen", text: "Pending", value: "Pending" },
{ key: "rej", text: "Rejected", value: "Rejected" }
];

const dateOptions = [
{ key: "tomorrow", text: "Tomorrow", value: "tomorrow" },
{ key: "next3days", text: "Next 3 days", value: "next3days" },
{ key: "nextweek", text: "Next week", value: "nextweek" },
{ key: "next2weeks", text: "Next 2 weeks", value: "next2weeks" },
{ key: "nextmonth", text: "Next month", value: "nextmonth" },
{ key: "next3months", text: "Next 3 months", value: "next3months" },
{ key: "nextyear", text: "Next year", value: "nextyear" },
{ key: "yesterday", text: "Yesterday", value: "yesterday" },
{ key: "last3days", text: "Last 3 days", value: "last3days" },
{ key: "lastweek", text: "Last week", value: "lastweek" },
{ key: "last2weeks", text: "Last 2 weeks", value: "last2weeks" },
{ key: "lastmonth", text: "Last month", value: "lastmonth" },
{ key: "last3months", text: "Last 3 months", value: "last3months" },
{ key: "lastyear", text: "Last year", value: "lastyear" },
];

function SearchBar({ className, onFilterChange, fluid = false }: Props) {
const [filters, setFilters] = useState<Filters>({
title: "",
venue: "",
date: "",
status: ""
});

const setField = (field: keyof Filters, value: string) => {
Expand All @@ -51,6 +87,98 @@ function SearchBar({ className, onFilterChange, fluid = false }: Props) {
setField("venue", data.value as string);
};

const handleStatusChange = (
_event: React.SyntheticEvent<HTMLElement>,
data: DropdownProps
) => {
setField("status", data.value as string);
};

const handleDateChange = (
_event: React.SyntheticEvent<HTMLElement>,
data: DropdownProps,
) => {
const value = data.value as string;

if (!value) {
setField("date", "");
return;
}

const today = new Date();
let newDate = today;

switch (value) {
case "tomorrow":
newDate = addDays(today, 1);
break;
case "next3days":
newDate = addDays(today, 3);
break;
case "nextweek":
newDate = addWeeks(today, 1);
break;
case "next2weeks":
newDate = addWeeks(today, 2);
break;
case "nextmonth":
newDate = addMonths(today, 1);
break;
case "next3months":
newDate = addMonths(today, 3);
break;
case "nextyear":
newDate = addYears(today, 1);
break;
case "yesterday":
newDate = subDays(today, 1);
break;
case "last3days":
newDate = subDays(today, 3);
break;
case "lastweek":
newDate = subWeeks(today, 1);
break;
case "last2weeks":
newDate = subWeeks(today, 2);
break;
case "lastmonth":
newDate = subMonths(today, 1);
break;
case "last3months":
newDate = subMonths(today, 3);
break;
case "lastyear":
newDate = subYears(today, 1);
break;
}

setField("date", format(newDate, "yyyy-MM-dd"));
};

const currentShortcutValue = useMemo(() => {
if (!filters.date) return "";
const today = new Date();
const target = filters.date;

if (target === format(addDays(today, 1), "yyyy-MM-dd")) return "tomorrow";
if (target === format(addDays(today, 3), "yyyy-MM-dd")) return "next3days";
if (target === format(addWeeks(today, 1), "yyyy-MM-dd")) return "nextweek";
if (target === format(addWeeks(today, 2), "yyyy-MM-dd")) return "next2weeks";
if (target === format(addMonths(today, 1), "yyyy-MM-dd")) return "nextmonth";
if (target === format(addMonths(today, 3), "yyyy-MM-dd")) return "next3months";
if (target === format(addYears(today, 1), "yyyy-MM-dd")) return "nextyear";
if (target === format(subDays(today, 1), "yyyy-MM-dd")) return "yesterday";
if (target === format(subDays(today, 3), "yyyy-MM-dd")) return "last3days";
if (target === format(subWeeks(today, 1), "yyyy-MM-dd")) return "lastweek";
if (target === format(subWeeks(today, 2), "yyyy-MM-dd")) return "last2weeks";
if (target === format(subMonths(today, 1), "yyyy-MM-dd")) return "lastmonth";
if (target === format(subMonths(today, 3), "yyyy-MM-dd")) return "last3months";
if (target === format(subYears(today, 1), "yyyy-MM-dd")) return "lastyear";

return "";
}, [filters.date]);
Comment on lines +159 to +180
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The currentShortcutValue computation performs multiple date calculations and format operations on every render. While it's memoized with useMemo, the calculation itself is still expensive with 14 conditional branches. Consider extracting this logic into a helper function or simplifying by storing the shortcut value directly in state when a shortcut is selected, rather than reverse-engineering it from the date string.

Copilot uses AI. Check for mistakes.

return (
<div className={clsx(styles.container, fluid && styles.fluid, className)}>
<Input
Expand Down Expand Up @@ -81,20 +209,27 @@ function SearchBar({ className, onFilterChange, fluid = false }: Props) {
onChange={handleVenueChange}
/>

<Input
<Dropdown
fluid
selection
clearable
className={clsx(styles.input, styles.small)}
icon={
filters.date ? (
<Icon name="times" link onClick={() => setField("date", "")} />
) : (
<Icon name="calendar alternate outline" />
)
}
iconPosition="left"
input={{ type: "date", value: filters.date }}
onChange={(_, { value }) => setField("date", value)}
placeholder="Date"
options={dateOptions}
value={currentShortcutValue}
onChange={handleDateChange}
/>

<Dropdown
fluid
selection
search
clearable
className={clsx(styles.input, styles.small)}
placeholder="Status"
options={statusOptions}
value={filters.status}
onChange={handleStatusChange}
/>
</div>
);
Expand Down
56 changes: 39 additions & 17 deletions frontend/src/custom-hooks/use-table-state-admin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { parseISO, parse, isWithinInterval, startOfDay, endOfDay } from "date-fns";
import throttle from "lodash/throttle";
import { Key, useMemo, useState } from "react";
import { SortOrder } from "react-base-table";
import { Filters } from "../components/admin-search-bar/admin-search-bar";
import { DATE_FORMAT } from "../constants";

import { sort } from "../utils/transform-utils";

Expand All @@ -14,7 +16,16 @@ export type TableStateOptions = {
defaultSortBy?: SortBy;
};

export default function useTableState<T>(
interface FilterableItem {
title?: string;
venue?: {
name?: string;
} | null;
date?: string;
status?: string;
}
Comment on lines +18 to +26
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FilterableItem interface defines a date property, but the actual booking data uses eventDateString as the property name (as seen in booking-admin-table.tsx where EVENT_DATE_STRING is populated). This mismatch will cause the date filtering to fail since item.date will be undefined. The interface should define eventDateString?: string; instead of date?: string;, or the filtering logic should check for the correct property name.

Copilot uses AI. Check for mistakes.

export default function useTableState<T extends FilterableItem>(
data: T[],
{ defaultSortBy }: TableStateOptions = {},
) {
Expand All @@ -23,6 +34,7 @@ export default function useTableState<T>(
title: "",
venue: "",
date: "",
status: ""
});

const onFilterChange = useMemo(
Expand All @@ -31,24 +43,17 @@ export default function useTableState<T>(
);

const filteredData = useMemo(() => {
const { title, venue, date } = activeFilters;

const { title, venue, date, status } = activeFilters;
if (
data.length === 0 ||
(!title && !venue && !date)
(!title && !venue && !date && !status)
) {
return data;
}

const formattedDate = date
? new Date(date).toLocaleDateString("en-GB", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})
: "";

return data.filter((item: any) => {
return data.filter((datum) => {
const item = datum;
Comment on lines +54 to +55
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable item is assigned from datum without any transformation or purpose. This is redundant and adds no value. Either use datum directly throughout the filter function or remove this assignment.

Suggested change
return data.filter((datum) => {
const item = datum;
return data.filter((item) => {

Copilot uses AI. Check for mistakes.

const titleMatch =
!title ||
(item.title &&
Expand All @@ -59,11 +64,28 @@ export default function useTableState<T>(
(item.venue?.name &&
item.venue.name.toLowerCase().includes(venue.toLowerCase()));

const dateMatch =
!date ||
(item.eventDateString && item.eventDateString.includes(formattedDate));
const dateMatch = (() => {
if (!date) return true;

const filterDate = parseISO(date);
const today = new Date();

if (!item.date) return false;

const itemDate = parse(item.date, DATE_FORMAT, new Date());

const start = startOfDay(filterDate < today ? filterDate : today);
const end = endOfDay(filterDate > today ? filterDate : today);
Comment on lines +77 to +79
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date range logic creates an interval from the earlier of filterDate or today to the later of the two. This means selecting "Tomorrow" filters bookings between today and tomorrow, while "Yesterday" filters bookings between yesterday and today. This range-based filtering behavior is not intuitive given the shortcut names like "Tomorrow" or "Yesterday" which suggest single-day filtering. Consider clarifying the intended behavior: if these shortcuts should filter for a specific day, use a single-day range instead; if they should filter for ranges, consider renaming the shortcuts to be more explicit (e.g., "Through Tomorrow", "Since Yesterday").

Suggested change
const today = new Date();
if (!item.date) return false;
const itemDate = parse(item.date, DATE_FORMAT, new Date());
const start = startOfDay(filterDate < today ? filterDate : today);
const end = endOfDay(filterDate > today ? filterDate : today);
if (!item.date) return false;
const itemDate = parse(item.date, DATE_FORMAT, new Date());
const start = startOfDay(filterDate);
const end = endOfDay(filterDate);

Copilot uses AI. Check for mistakes.

return isWithinInterval(itemDate, { start, end });
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date filtering logic checks item.date, but the actual property name in the booking data is eventDateString (as defined in constants and used in the booking table). This will cause the filter to always return false for items, making the date filter non-functional. The code should check item.eventDateString instead.

Copilot uses AI. Check for mistakes.
})();

const statusMatch =
!status ||
(item.status &&
item.status.toLowerCase() === status.toLowerCase());

return titleMatch && venueMatch && dateMatch;
return titleMatch && venueMatch && dateMatch && statusMatch;
});
}, [data, activeFilters]);

Expand Down