M18: implement operational workspaces
This commit is contained in:
@@ -10,6 +10,17 @@ import { Pagination } from "../components/Pagination";
|
||||
|
||||
const STATUS_OPTIONS = ["reserved", "active", "returned", "cancelled", "blocked"];
|
||||
|
||||
function localDateString(date = new Date()): string {
|
||||
const offset = date.getTimezoneOffset() * 60_000;
|
||||
return new Date(date.getTime() - offset).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function nextLocalDay(value: string): string {
|
||||
const date = new Date(`${value}T12:00:00`);
|
||||
date.setDate(date.getDate() + 1);
|
||||
return localDateString(date);
|
||||
}
|
||||
|
||||
export function Bookings() {
|
||||
const { t } = useTranslation("bookings");
|
||||
const { formatShortDate } = useLocaleFormat();
|
||||
@@ -18,6 +29,10 @@ export function Bookings() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const status = searchParams.get("status") ?? "";
|
||||
const query = searchParams.get("q") ?? "";
|
||||
const startsFrom = searchParams.get("from") ?? "";
|
||||
const startsTo = searchParams.get("to") ?? "";
|
||||
const location = searchParams.get("location") ?? "";
|
||||
const sort = searchParams.get("sort") ?? "operational";
|
||||
const page = Math.max(1, Number(searchParams.get("page") ?? "1") || 1);
|
||||
|
||||
function updateFilters(updates: Record<string, string | number | null>) {
|
||||
@@ -35,11 +50,15 @@ export function Bookings() {
|
||||
const params = new URLSearchParams({ page: String(page), page_size: "25" });
|
||||
if (status) params.set("status", status);
|
||||
if (query) params.set("query", query);
|
||||
if (startsFrom) params.set("starts_from", new Date(`${startsFrom}T00:00:00`).toISOString());
|
||||
if (startsTo) params.set("starts_to", new Date(`${nextLocalDay(startsTo)}T00:00:00`).toISOString());
|
||||
if (location) params.set("location", location);
|
||||
params.set("sort", sort);
|
||||
api
|
||||
.get<Page<Booking>>(`/api/v1/bookings?${params.toString()}`)
|
||||
.then(setBookings)
|
||||
.catch(() => setError(t("list.unavailable")));
|
||||
}, [page, query, status, t]);
|
||||
}, [page, query, status, startsFrom, startsTo, location, sort, t]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
@@ -57,7 +76,16 @@ export function Bookings() {
|
||||
{STATUS_OPTIONS.map((value) => <option key={value} value={value}>{t(`statuses.${value}`)}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label>{t("list.fromLabel")}<input type="date" value={startsFrom} onChange={(event) => updateFilters({ from: event.target.value, page: 1 })} /></label>
|
||||
<label>{t("list.toLabel")}<input type="date" value={startsTo} onChange={(event) => updateFilters({ to: event.target.value, page: 1 })} /></label>
|
||||
<label>{t("list.locationLabel")}<input value={location} onChange={(event) => updateFilters({ location: event.target.value, page: 1 })} placeholder={t("list.locationPlaceholder")} /></label>
|
||||
<label>{t("list.sortLabel")}<select value={sort} onChange={(event) => updateFilters({ sort: event.target.value, page: 1 })}><option value="operational">{t("list.sortOperational")}</option><option value="starts_asc">{t("list.sortAscending")}</option><option value="starts_desc">{t("list.sortDescending")}</option></select></label>
|
||||
</form>
|
||||
<div className="filter-presets" aria-label={t("list.presetsLabel")}>
|
||||
<button type="button" onClick={() => updateFilters({ from: localDateString(), to: localDateString(), status: null, page: 1 })}>{t("list.todayPreset")}</button>
|
||||
<button type="button" onClick={() => updateFilters({ from: localDateString(), to: null, status: "reserved", sort: "starts_asc", page: 1 })}>{t("list.upcomingPreset")}</button>
|
||||
<button type="button" onClick={() => setSearchParams(new URLSearchParams())}>{t("list.clearFilters")}</button>
|
||||
</div>
|
||||
|
||||
{error && <ErrorState message={error} />}
|
||||
{!error && !bookings && <LoadingState label={t("list.loading")} />}
|
||||
|
||||
Reference in New Issue
Block a user