reservation-front/src/pages/EventReservations.tsx
2026-07-15 16:31:59 +03:30

173 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import Icon from "../components/Icon";
import { CafeEvent, mockCafeEvents } from "../data/mockCafeEvents";
import { EventReservation, fetchEventReservations } from "../services/campaignApi";
import { getCafeUuid } from "../utils/cafe";
import HostBanner from "../assets/images/banners/Host-banner.jpg";
import { formatPersianNumber } from "../utils/formatNumber";
function toEnglishDigits(value: string) {
const persianDigits = "۰۱۲۳۴۵۶۷۸۹";
return value.replace(/[۰-۹]/g, (digit) =>
String(persianDigits.indexOf(digit)),
);
}
function reservationMatches(reservation: EventReservation, query: string) {
if (!query) return true;
const haystack = toEnglishDigits(
`${reservation.full_name} ${reservation.phone_number} ${reservation.people_count} ${formatPersianNumber(reservation.people_count)} ${reservation.reserve_code}`,
).toLowerCase();
return haystack.includes(toEnglishDigits(query).toLowerCase());
}
function downloadReservationsCsv(reservations: EventReservation[]) {
const header = ["کد رزرو", "تعداد", "نام", "شماره تماس"];
const rows = reservations.map((r) => [
r.reserve_code,
String(r.people_count),
r.full_name,
toEnglishDigits(r.phone_number),
]);
const csvContent = [header, ...rows]
.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(","))
.join("\n");
const blob = new Blob(["" + csvContent], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "reservations.csv";
link.click();
URL.revokeObjectURL(url);
}
function ReservationCard({
reservation,
selected,
onToggle,
}: {
reservation: EventReservation;
selected: boolean;
onToggle: () => void;
}) {
return (
<div className="flex items-center justify-between rounded-2xl shadow-[0px_4px_12px_0px_#0000001F] p-6">
<div className="flex flex-col items-start gap-2 text-right">
<div className="flex items-center gap-2">
<span className="font-medium rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
{reservation.reserve_code}
</span>
<span className="font-medium rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
{formatPersianNumber(reservation.people_count)} نفر
</span>
</div>
<p className="text-gray-900">{reservation.full_name}</p>
<p className="text-gray-500">{reservation.phone_number}</p>
</div>
<button
type="button"
onClick={onToggle}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 border-[#223D5D] ${selected ? "bg-[#223D5D]" : "bg-transparent"}`}
>
{selected && <Icon name="check" className="h-3 w-3 text-white" />}
</button>
</div>
);
}
export default function EventReservations() {
const { id } = useParams();
const location = useLocation();
const navigate = useNavigate();
const [query, setQuery] = useState("");
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [reservations, setReservations] = useState<EventReservation[]>([]);
const event: CafeEvent | undefined =
(location.state as { event?: CafeEvent } | null)?.event ??
mockCafeEvents.find((e) => e.id === Number(id));
useEffect(() => {
const cafeUuid = getCafeUuid();
if (!cafeUuid || !id) return;
fetchEventReservations(cafeUuid, id)
.then(setReservations)
.catch(() => {});
}, [id]);
const filteredReservations = useMemo(
() =>
reservations.filter((reservation) =>
reservationMatches(reservation, query),
),
[reservations, query],
);
const toggleSelected = (reservationId: string) => {
setSelectedIds((prev) =>
prev.includes(reservationId)
? prev.filter((id) => id !== reservationId)
: [...prev, reservationId],
);
};
const handleDownload = () => {
downloadReservationsCsv(reservations);
};
return (
<div className="min-h-screen bg-white pb-24">
<img src={HostBanner} alt="" className="w-full" />
<button
type="button"
onClick={() => navigate(-1)}
className="mt-4 flex w-full items-center justify-start gap-2 px-4"
>
<Icon name="arrowRight" className="h-5 w-5 -scale-x-100 text-gray-900" />
<span className="text-md font-semibold text-gray-900">
رزروهای {event?.matchLabel ?? ""}
</span>
</button>
<div className="relative mt-4 px-4">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="جستجو"
className="w-full rounded-2xl shadow-[0px_4px_12px_0px_#0000001F] bg-white py-3 pr-6 pl-4 text-right placeholder:font-light placeholder:text-[#999999]"
/>
<Icon
name="search"
className="pointer-events-none absolute left-8 top-1/2 h-5 w-5 -translate-y-1/2 text-[#666666]"
/>
</div>
<div className="mt-4 flex flex-col gap-3 px-4">
{filteredReservations.map((reservation) => (
<ReservationCard
key={reservation.uuid}
reservation={reservation}
selected={selectedIds.includes(reservation.uuid)}
onToggle={() => toggleSelected(reservation.uuid)}
/>
))}
</div>
<div className="fixed inset-x-0 bottom-0 bg-white px-4 py-4">
<button
type="button"
onClick={handleDownload}
className="w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
دانلود لیست
</button>
</div>
</div>
);
}