173 lines
5.8 KiB
TypeScript
173 lines
5.8 KiB
TypeScript
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>
|
||
);
|
||
}
|