541 lines
28 KiB
TypeScript
541 lines
28 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||
import { Helmet } from "@/lib/helmet";
|
||
import { Navigate, useNavigate } from "@/lib/router";
|
||
import { useAuth } from "@/contexts/AuthContext";
|
||
import OutcomePage, { type OutcomeAction, type OutcomeTone } from "@/components/OutcomePage";
|
||
import Markdown from "@/components/Markdown";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { api } from "@/lib/api";
|
||
import { buildAuthPath } from "@/lib/auth-return";
|
||
import { getEventPath } from "@/lib/event-ui";
|
||
import { downloadRegistrationReceipt } from "@/lib/registration-receipt-pdf";
|
||
import {
|
||
getFinalPriceToman,
|
||
getRegistrationHubDisplayState,
|
||
getRegistrationHubGate,
|
||
getRegistrationHubGateCopy,
|
||
normalizeCouponAmounts,
|
||
} from "@/lib/registration-hub-display";
|
||
import type { RegistrationHubSchema, RegistrationHubState } from "@/lib/types";
|
||
import { displayIdentifier, formatJalali, formatNumberPersian, formatToman, resolveErrorMessage } from "@/lib/utils";
|
||
import { siteUrl } from "@/lib/site";
|
||
|
||
type Props = {
|
||
slug: string;
|
||
registrationId?: string;
|
||
paymentId?: string;
|
||
};
|
||
|
||
type LoadState = "loading" | "ready" | "not_found" | "error";
|
||
|
||
type CouponResult = {
|
||
valid: boolean;
|
||
discountAmount: number;
|
||
finalPrice: number;
|
||
message: string;
|
||
};
|
||
|
||
const pollingStates = new Set<RegistrationHubState>([
|
||
"active_checkout",
|
||
"verification_pending",
|
||
"manual_review",
|
||
"refund_pending",
|
||
"refund_manual_review",
|
||
]);
|
||
|
||
const stateCopy: Record<RegistrationHubState, { status: string; title: string; subtitle: string }> = {
|
||
ready: {
|
||
status: "آمادهٔ ثبتنام",
|
||
title: "برای ثبتنام در رویداد آمادهاید؟",
|
||
subtitle: "",
|
||
},
|
||
active_checkout: {
|
||
status: "پرداخت در حال انجام",
|
||
title: "ثبتنام شما موقتاً ظرفیت را نگه داشته است",
|
||
subtitle: "میتوانید پرداخت همین ثبتنام را ادامه دهید. مهلت از زمان رزرو اولیه محاسبه میشود و با بازگشت به صفحه تمدید نخواهد شد.",
|
||
},
|
||
legacy_pending: {
|
||
status: "ثبتنام قدیمی در انتظار بررسی",
|
||
title: "وضعیت این ثبتنام هنوز نهایی نیست",
|
||
subtitle: "این ثبتنام پیش از فعالشدن مهلت پرداخت ایجاد شده است. برای جلوگیری از پرداخت تکراری، وضعیت آن را با پشتیبانی بررسی کنید.",
|
||
},
|
||
verification_pending: {
|
||
status: "در انتظار تأیید درگاه",
|
||
title: "در حال دریافت نتیجهٔ پرداخت...",
|
||
subtitle: "در صورتی که هزینهٔ رویداد از حساب شما کم شده است، تا دریافت نتیجهٔ قطعی، پرداخت دیگری برای این رویداد ثبت نکنید.",
|
||
},
|
||
manual_review: {
|
||
status: "نیازمند بررسی پشتیبانی",
|
||
title: "خطا در هنگام تأیید تراکنش",
|
||
subtitle: "امکان پیگیری خودکار وضعیت تراکنش از طریق درگاه پرداخت امکانپذیر نیست. لطفاً در صورتی که هزینهٔ رویداد از حساب شما کسر شده است، رسید پرداخت را برای بررسی تراکنش به پشتیبانی انجمن ارسال کنید.",
|
||
},
|
||
paid_success: {
|
||
status: "پرداخت تأیید شد",
|
||
title: "ثبتنام شما تکمیل شد",
|
||
subtitle: "ثبتنام شما در این رویداد با موفقیت در سامانه ثبت شد. در صورت بروز هر گونه مشکل هنگام ورود به رویداد رسید پرداخت را همراه خود داشته باشید.",
|
||
},
|
||
free_success: {
|
||
status: "ثبتنام تأیید شد",
|
||
title: "ثبتنام شما تکمیل شد",
|
||
subtitle: "ثبتنام شما در این رویداد با موفقیت در سامانه ثبت شد. در صورت بروز هر گونه مشکل هنگام ورود به رویداد رسید پرداخت را همراه خود داشته باشید.",
|
||
},
|
||
failed: {
|
||
status: "پرداخت ناموفق",
|
||
title: "این پرداخت تأیید نشد",
|
||
subtitle: "این تلاش پرداخت بهصورت قطعی ناموفق ثبت شده است. اگر ثبتنام هنوز باز باشد، میتوانید یک تلاش تازه آغاز کنید.",
|
||
},
|
||
cancelled: {
|
||
status: "ثبتنام لغو شده",
|
||
title: "این ثبتنام لغو شده است",
|
||
subtitle: "در صورت تمایل به ثبتنام مجدد میتوانید از طریق دکمهٔ پایین صفحه در رویداد ثبتنام کنید.",
|
||
},
|
||
expired: {
|
||
status: "مهلت پرداخت پایان یافت",
|
||
title: "مهلت ثبتنام شما به پایان رسید.",
|
||
subtitle: "مهلت ۱۵ دقیقهای پرداخت به پایان رسیده و ظرفیت این رزرو آزاد شده است. در صورت تمایل میتوانید مجدداً ثبتنام کنید.",
|
||
},
|
||
refund_pending: {
|
||
status: "بازگشت وجه در حال پیگیری",
|
||
title: "پرداخت دریافت شد اما ثبتنام لغو است",
|
||
subtitle: "بازگشت وجه از درگاه در حال پیگیری است. تا روشنشدن نتیجه، پرداخت دیگری آغاز نکنید.",
|
||
},
|
||
refund_manual_review: {
|
||
status: "بازگشت وجه نیازمند بررسی",
|
||
title: "بازگشت وجه هنوز تأیید نشده است",
|
||
subtitle: "ثبتنام لغو شده و وضعیت بازگشت وجه نیاز به بررسی پشتیبانی دارد. کد پیگیری را برای تماس با انجمن نگه دارید.",
|
||
},
|
||
refund_succeeded: {
|
||
status: "وجه بازگردانده شد",
|
||
title: "بازگشت وجه تأیید شد",
|
||
subtitle: "درگاه بازگشت وجه را تأیید کرده است. برای ثبتنام دوباره، وضعیت رویداد را بررسی کنید.",
|
||
},
|
||
};
|
||
|
||
function checkoutTimeRemaining(expiresAt?: string | null) {
|
||
if (!expiresAt) return "—";
|
||
const remaining = Math.max(0, Date.parse(expiresAt) - Date.now());
|
||
const minutes = Math.floor(remaining / 60_000);
|
||
const seconds = Math.floor((remaining % 60_000) / 1000);
|
||
return `${minutes.toLocaleString("fa-IR")} دقیقه و ${seconds.toLocaleString("fa-IR")} ثانیه`;
|
||
}
|
||
|
||
function formatEventDateTimeRange(start?: string | null, end?: string | null) {
|
||
if (!start) return end ? formatJalali(end) : "—";
|
||
if (!end) return formatJalali(start);
|
||
const startDate = formatJalali(start, false);
|
||
if (startDate !== "—" && startDate === formatJalali(end, false)) {
|
||
const formatTime = (value: string) => new Intl.DateTimeFormat("fa-IR", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}).format(new Date(value));
|
||
return `${startDate}، ساعت ${formatTime(start)} تا ${formatTime(end)}`;
|
||
}
|
||
return `${formatJalali(start)} تا ${formatJalali(end)}`;
|
||
}
|
||
|
||
export default function EventRegistrationHub({ slug, registrationId, paymentId }: Props) {
|
||
const { isAuthenticated, loading: authLoading } = useAuth();
|
||
const navigate = useNavigate();
|
||
const receiptRef = useRef<HTMLDivElement | null>(null);
|
||
const busyRef = useRef(false);
|
||
const dataRef = useRef<RegistrationHubSchema | null>(null);
|
||
const [data, setData] = useState<RegistrationHubSchema | null>(null);
|
||
const [loadState, setLoadState] = useState<LoadState>("loading");
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
const [refreshToken, setRefreshToken] = useState(0);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [receiptDownloading, setReceiptDownloading] = useState(false);
|
||
const [actionError, setActionError] = useState<string | null>(null);
|
||
const [refreshError, setRefreshError] = useState(false);
|
||
const [couponCode, setCouponCode] = useState("");
|
||
const [couponResult, setCouponResult] = useState<CouponResult | null>(null);
|
||
const [couponVerifying, setCouponVerifying] = useState(false);
|
||
|
||
useEffect(() => {
|
||
dataRef.current = null;
|
||
setData(null);
|
||
setLoadState("loading");
|
||
setRefreshError(false);
|
||
setActionError(null);
|
||
setCouponCode("");
|
||
setCouponResult(null);
|
||
}, [paymentId, registrationId, slug]);
|
||
|
||
const hubPath = useMemo(() => {
|
||
const query = new URLSearchParams();
|
||
if (registrationId) query.set("registration_id", registrationId);
|
||
else if (paymentId) query.set("payment_id", paymentId);
|
||
return `/events/${encodeURIComponent(slug)}/registration${query.size ? `?${query}` : ""}`;
|
||
}, [paymentId, registrationId, slug]);
|
||
|
||
const refresh = useCallback(async () => {
|
||
const result = await api.getRegistrationHubStatus(slug, {
|
||
registration_id: registrationId,
|
||
payment_id: paymentId ? Number(paymentId) : null,
|
||
});
|
||
dataRef.current = result;
|
||
setData(result);
|
||
setLoadState("ready");
|
||
return result;
|
||
}, [paymentId, registrationId, slug]);
|
||
|
||
useEffect(() => {
|
||
if (!isAuthenticated || authLoading) return;
|
||
let active = true;
|
||
let timer: number | undefined;
|
||
let delay = 5_000;
|
||
let failedReads = 0;
|
||
|
||
const load = async () => {
|
||
setRefreshing(true);
|
||
try {
|
||
const result = await api.getRegistrationHubStatus(slug, {
|
||
registration_id: registrationId,
|
||
payment_id: paymentId ? Number(paymentId) : null,
|
||
});
|
||
if (!active) return;
|
||
dataRef.current = result;
|
||
setData(result);
|
||
setLoadState("ready");
|
||
setRefreshError(false);
|
||
failedReads = 0;
|
||
if (pollingStates.has(result.state)) {
|
||
timer = window.setTimeout(load, delay);
|
||
delay = Math.min(delay * 2, 30_000);
|
||
}
|
||
} catch (error) {
|
||
if (!active) return;
|
||
failedReads += 1;
|
||
const message = resolveErrorMessage(error, "");
|
||
if (!dataRef.current && (message.includes("404") || message.includes("پیدا نشد"))) {
|
||
setLoadState("not_found");
|
||
} else if (dataRef.current) {
|
||
setRefreshError(true);
|
||
if (pollingStates.has(dataRef.current.state) && failedReads < 5) {
|
||
timer = window.setTimeout(load, delay);
|
||
delay = Math.min(delay * 2, 30_000);
|
||
}
|
||
} else {
|
||
setLoadState("error");
|
||
}
|
||
} finally {
|
||
if (active) setRefreshing(false);
|
||
}
|
||
};
|
||
|
||
setLoadState((current) => current === "ready" ? current : "loading");
|
||
void load();
|
||
return () => {
|
||
active = false;
|
||
if (timer !== undefined) window.clearTimeout(timer);
|
||
};
|
||
}, [authLoading, isAuthenticated, paymentId, refreshToken, registrationId, slug]);
|
||
|
||
if (authLoading) {
|
||
return <OutcomePage tone="checking" statusLabel="در حال بررسی حساب" title="در حال بارگذاری ثبتنام..." subtitle="" refreshing />;
|
||
}
|
||
if (!isAuthenticated) {
|
||
return <Navigate to={buildAuthPath(hubPath)} replace />;
|
||
}
|
||
|
||
const event = data?.event;
|
||
const state = data?.state;
|
||
const presentationState = state ? getRegistrationHubDisplayState(state) : undefined;
|
||
const gate = data && state === "ready" && !data.can_register ? getRegistrationHubGate(data.event) : null;
|
||
const gateCopy = getRegistrationHubGateCopy(gate);
|
||
const copy = gateCopy ?? (presentationState ? stateCopy[presentationState] : null);
|
||
const canApplyCoupon = Boolean(
|
||
data
|
||
&& event
|
||
&& data.can_register
|
||
&& event.price > 0
|
||
&& ["ready", "failed", "cancelled", "expired"].includes(state || ""),
|
||
);
|
||
const verifiedCoupon = couponResult?.valid ? couponCode.trim() : "";
|
||
const tone: OutcomeTone = !data || loadState === "not_found" || loadState === "error"
|
||
? loadState === "loading" ? "checking" : "unknown"
|
||
: state === "paid_success" || state === "free_success"
|
||
? "success"
|
||
: state === "failed"
|
||
? "failed"
|
||
: state === "cancelled" || state === "expired"
|
||
? "canceled"
|
||
: state === "refund_succeeded"
|
||
? "success"
|
||
: state === "ready" && data.can_register
|
||
? "unknown"
|
||
: "pending";
|
||
|
||
const eventHref = event ? getEventPath({ id: event.id, slug: event.slug }) : "/events";
|
||
const money = (amount?: number | null) => typeof amount === "number" ? formatToman(amount) : "—";
|
||
const hasSettledRegistration = state === "paid_success" || state === "free_success";
|
||
const refreshNow = () => setRefreshToken((value) => value + 1);
|
||
|
||
const startCheckout = async (coupon?: string) => {
|
||
if (!data || busyRef.current) return;
|
||
busyRef.current = true;
|
||
setSubmitting(true);
|
||
setActionError(null);
|
||
try {
|
||
const registration = state === "active_checkout" && data.registration
|
||
? data.registration
|
||
: await api.registerForEvent(data.event.id, coupon || null);
|
||
|
||
if (registration.status === "confirmed" || (registration.final_price ?? data.event.price) <= 0) {
|
||
navigate(`${getEventPath(data.event)}/registration?registration_id=${encodeURIComponent(registration.ticket_id)}`, { replace: true });
|
||
return;
|
||
}
|
||
|
||
const payment = await api.createPayment({
|
||
event_id: data.event.id,
|
||
description: `پرداخت رویداد: ${data.event.title}`,
|
||
discount_code: coupon || null,
|
||
idempotency_key: registration.ticket_id,
|
||
});
|
||
if (payment.start_pay_url) {
|
||
window.location.href = payment.start_pay_url;
|
||
return;
|
||
}
|
||
|
||
navigate(`${getEventPath(data.event)}/registration?registration_id=${encodeURIComponent(registration.ticket_id)}`, { replace: true });
|
||
setRefreshToken((value) => value + 1);
|
||
} catch (error) {
|
||
setActionError(resolveErrorMessage(error, "ادامهٔ ثبتنام انجام نشد. وضعیت را دوباره بررسی کنید."));
|
||
setRefreshToken((value) => value + 1);
|
||
} finally {
|
||
busyRef.current = false;
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const verifyCoupon = async () => {
|
||
if (!data || !couponCode.trim() || couponVerifying || submitting) return;
|
||
setCouponVerifying(true);
|
||
setActionError(null);
|
||
try {
|
||
const raw = await api.checkDiscountCode(data.event.id, couponCode.trim());
|
||
const normalized = normalizeCouponAmounts(raw);
|
||
setCouponResult({
|
||
valid: true,
|
||
discountAmount: normalized.discount_amount,
|
||
finalPrice: normalized.final_price,
|
||
message: "کد تخفیف با موفقیت اعمال شد.",
|
||
});
|
||
} catch (error) {
|
||
setCouponResult({
|
||
valid: false,
|
||
discountAmount: 0,
|
||
finalPrice: data.event.price,
|
||
message: resolveErrorMessage(error, "کد تخفیف معتبر نیست."),
|
||
});
|
||
} finally {
|
||
setCouponVerifying(false);
|
||
}
|
||
};
|
||
|
||
const cancelCheckout = async () => {
|
||
if (!data || busyRef.current) return;
|
||
busyRef.current = true;
|
||
setSubmitting(true);
|
||
setActionError(null);
|
||
try {
|
||
await api.cancelRegistrationCheckout(data.event.id);
|
||
const result = await refresh();
|
||
setActionError(result.state === "verification_pending" || result.state === "refund_pending" || result.state === "refund_manual_review"
|
||
? "ظرفیت آزاد شد؛ تراکنش قبلی هنوز در حال تعیینتکلیف است. تا دریافت نتیجه، پرداخت دیگری آغاز نکنید."
|
||
: null);
|
||
} catch (error) {
|
||
setActionError(resolveErrorMessage(error, "لغو رزرو انجام نشد. دوباره وضعیت را بررسی کنید."));
|
||
} finally {
|
||
busyRef.current = false;
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const downloadReceipt = async () => {
|
||
if (!receiptRef.current || !data || !hasSettledRegistration || receiptDownloading) return;
|
||
setReceiptDownloading(true);
|
||
try {
|
||
await downloadRegistrationReceipt(
|
||
receiptRef.current,
|
||
`registration-${data.registration?.ticket_id || data.payment?.id || data.event.id}.pdf`,
|
||
);
|
||
} catch (error) {
|
||
setActionError(resolveErrorMessage(error, "دانلود رسید انجام نشد. دوباره تلاش کنید."));
|
||
} finally {
|
||
setReceiptDownloading(false);
|
||
}
|
||
};
|
||
|
||
const primaryAction: OutcomeAction | undefined = (() => {
|
||
if (loadState === "error") {
|
||
return { label: "بررسی دوباره", onClick: refreshNow, disabled: refreshing };
|
||
}
|
||
if (!data || !state) return undefined;
|
||
if (state === "ready" && data.can_register) {
|
||
return data.event.price > 0
|
||
? { label: submitting ? "در حال آمادهسازی..." : couponResult?.valid && couponResult.finalPrice <= 0 ? "ثبتنام رایگان با کد تخفیف" : "ثبتنام و ادامهٔ پرداخت", onClick: () => void startCheckout(verifiedCoupon || undefined), disabled: submitting || couponVerifying || Boolean(couponCode.trim() && !couponResult?.valid) }
|
||
: { label: "ثبتنام رایگان", onClick: () => void startCheckout(), disabled: submitting };
|
||
}
|
||
if (state === "active_checkout") {
|
||
return data.can_resume
|
||
? { label: submitting ? "در حال ادامهٔ پرداخت…" : "ادامهٔ پرداخت", onClick: () => void startCheckout(), disabled: submitting }
|
||
: { label: "بررسی دوباره", onClick: refreshNow, disabled: refreshing };
|
||
}
|
||
if (state === "paid_success" || state === "free_success") {
|
||
return { label: "مشاهدهٔ جزئیات رویداد", href: eventHref };
|
||
}
|
||
if (state === "failed" || state === "cancelled" || state === "expired") {
|
||
return data.can_register
|
||
? data.event.price > 0
|
||
? { label: submitting ? "در حال آمادهسازی..." : couponResult?.valid && couponResult.finalPrice <= 0 ? "ثبتنام رایگان با کد تخفیف" : "تلاش دوباره برای ثبتنام", onClick: () => void startCheckout(verifiedCoupon || undefined), disabled: submitting || couponVerifying || Boolean(couponCode.trim() && !couponResult?.valid) }
|
||
: { label: "ثبتنام دوباره", onClick: () => void startCheckout(), disabled: submitting }
|
||
: { label: "مشاهدهٔ جزئیات رویداد", href: eventHref };
|
||
}
|
||
if (state === "legacy_pending" || state === "manual_review" || state === "refund_manual_review") {
|
||
return { label: "مشاهدهٔ پرداختهای من", href: "/profile?tab=payments" };
|
||
}
|
||
if (state === "refund_pending" || state === "refund_succeeded") {
|
||
return { label: "مشاهدهٔ پرداختهای من", href: "/profile?tab=payments" };
|
||
}
|
||
if (state === "verification_pending") {
|
||
return { label: refreshing ? "در حال بررسی…" : "بررسی دوباره", onClick: refreshNow, disabled: refreshing };
|
||
}
|
||
return {
|
||
label: data.can_register ? "شروع ثبتنام" : "مشاهدهٔ رویدادها",
|
||
...(data.can_register
|
||
? { onClick: () => data.event.price > 0 ? void startCheckout(verifiedCoupon || undefined) : void startCheckout(), disabled: submitting || couponVerifying || Boolean(couponCode.trim() && !couponResult?.valid) }
|
||
: { href: "/events" }),
|
||
};
|
||
})();
|
||
|
||
const secondaryActions: OutcomeAction[] = [];
|
||
if (data?.can_cancel && state === "active_checkout") {
|
||
secondaryActions.push({ label: "لغو پرداخت و آزادکردن ظرفیت", onClick: () => void cancelCheckout(), disabled: submitting });
|
||
}
|
||
if (state === "paid_success" || state === "free_success") {
|
||
secondaryActions.push(
|
||
{
|
||
label: receiptDownloading ? "در حال ساخت فایل PDF..." : "دانلود رسید PDF",
|
||
onClick: () => void downloadReceipt(),
|
||
disabled: receiptDownloading,
|
||
loading: receiptDownloading,
|
||
},
|
||
);
|
||
}
|
||
if (["verification_pending", "manual_review", "refund_pending", "refund_manual_review"].includes(state || "")) {
|
||
secondaryActions.push({ label: "جزئیات رویداد", href: eventHref });
|
||
}
|
||
if (loadState === "not_found" || loadState === "error") {
|
||
secondaryActions.push({ label: "مشاهدهٔ رویدادها", href: "/events" });
|
||
}
|
||
|
||
const details = data
|
||
? [
|
||
{ label: "رویداد", value: data.event.title },
|
||
{ label: "زمان برگزاری", value: formatEventDateTimeRange(data.event.start_time, data.event.end_time) },
|
||
{ label: "هزینهٔ رویداد", value: data.event.price <= 0 ? "رایگان" : money(data.event.price) },
|
||
...(data.registration ? [{ label: "وضعیت ثبتنام", value: data.registration.status === "confirmed" || data.registration.status === "attended" ? "تأییدشده" : data.registration.status === "pending" ? "در انتظار" : "لغوشده" }] : []),
|
||
...(data.registration?.checkout_expires_at && state === "active_checkout" ? [{ label: "زمان باقیماندهٔ رزرو", value: checkoutTimeRemaining(data.registration.checkout_expires_at) }] : []),
|
||
...(data.payment ? [
|
||
{ label: "وضعیت پرداخت", value: data.payment.status === "paid" ? "پرداخت تأییدشده" : data.payment.status === "failed" ? "ناموفق" : data.payment.status === "canceled" ? "لغوشده" : "در انتظار تأیید" },
|
||
{ label: "مبلغ پرداخت", value: money(data.payment.amount) },
|
||
...(data.payment.ref_id ? [{ label: "کد پیگیری درگاه", value: <code dir="ltr" className="font-mono">{data.payment.ref_id}</code> }] : []),
|
||
] : []),
|
||
...(data.registration ? [{ label: "کد ثبتنام", value: <code dir="ltr" className="font-mono">{displayIdentifier(data.registration.ticket_id)}</code> }] : []),
|
||
...(data.registration?.registered_at ? [{ label: "تاریخ درخواست", value: formatJalali(data.registration.registered_at) }] : []),
|
||
]
|
||
: [];
|
||
|
||
const title = copy?.title ?? (loadState === "not_found" ? "ثبتنام یا پرداخت پیدا نشد" : loadState === "error" ? "وضعیت ثبتنام در دسترس نیست" : "در حال دریافت وضعیت ثبتنام...");
|
||
const subtitle = copy?.subtitle ?? (loadState === "not_found"
|
||
? "این شناسه برای حساب کاربری شما و این رویداد پیدا نشد. از صفحهٔ رویدادها وضعیت ثبتنامهای خود را بررسی کنید."
|
||
: loadState === "error"
|
||
? "ارتباط با سامانه برقرار نشد. این خطا بهتنهایی به معنی ناموفق یا لغوشدن پرداخت نیست؛ دوباره تلاش کنید."
|
||
: "");
|
||
const statusLabel = copy?.status ?? (loadState === "not_found" ? "شناسه پیدا نشد" : loadState === "error" ? "خطای ارتباط" : "در حال بررسی");
|
||
const pageTitle = copy?.status ?? "وضعیت ثبتنام";
|
||
|
||
return (
|
||
<>
|
||
<Helmet>
|
||
<title>{`${pageTitle} | انجمن نمونه`}</title>
|
||
<meta name="description" content={subtitle} />
|
||
<meta name="robots" content="noindex, nofollow" />
|
||
<link rel="canonical" href={`${siteUrl}/events/${encodeURIComponent(slug)}/registration`} />
|
||
</Helmet>
|
||
<OutcomePage
|
||
tone={tone}
|
||
statusLabel={statusLabel}
|
||
title={title}
|
||
subtitle={subtitle}
|
||
event={event ? {
|
||
title: event.title,
|
||
imageUrl: event.image_url,
|
||
href: eventHref,
|
||
caption: state === "paid_success" || state === "free_success" ? "ثبتنام شما برای این رویداد تأیید شده است." : undefined,
|
||
} : undefined}
|
||
details={details}
|
||
reference={data?.payment?.ref_id || data?.registration?.ticket_id || null}
|
||
referenceLabel={data?.payment?.ref_id ? "کد پیگیری درگاه" : "کد ثبتنام"}
|
||
support={presentationState === "verification_pending" || presentationState === "manual_review" || presentationState === "refund_manual_review" || loadState === "error" || loadState === "not_found"}
|
||
supportDescription={presentationState === "verification_pending"
|
||
? "اگر هزینهٔ رویداد از حساب شما کسر شده است، رسید پرداخت و کد پیگیری را برای بررسی وضعیت با انجمن در میان بگذارید. تا دریافت نتیجهٔ قطعی، پرداخت دیگری آغاز نکنید."
|
||
: presentationState === "manual_review"
|
||
? "رسید پرداخت و کد پیگیری را برای بررسی تراکنش به پشتیبانی انجمن ارسال کنید."
|
||
: "شناسهٔ ثبتنام یا پرداخت را نگه دارید و برای بررسی با انجمن تماس بگیرید. تا روشنشدن وضعیت، پرداخت دیگری آغاز نکنید."}
|
||
refreshing={refreshing}
|
||
primaryAction={primaryAction}
|
||
secondaryActions={secondaryActions}
|
||
receiptRef={receiptRef}
|
||
>
|
||
{canApplyCoupon ? (
|
||
<section className="space-y-3" aria-label="کد تخفیف">
|
||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
|
||
<Input
|
||
id="registration-coupon"
|
||
value={couponCode}
|
||
onChange={(eventChange) => {
|
||
setCouponCode(eventChange.target.value);
|
||
setCouponResult(null);
|
||
setActionError(null);
|
||
}}
|
||
placeholder="کد تخفیف (اختیاری)"
|
||
aria-label="کد تخفیف (اختیاری)"
|
||
aria-describedby={couponResult ? "registration-coupon-result" : undefined}
|
||
className="text-right"
|
||
/>
|
||
<Button type="button" variant="outline" disabled={!couponCode.trim() || couponVerifying || submitting} onClick={() => void verifyCoupon()}>
|
||
{couponVerifying ? "در حال بررسی..." : "بررسی کد تخفیف"}
|
||
</Button>
|
||
</div>
|
||
{couponResult ? (
|
||
<p id="registration-coupon-result" role={couponResult.valid ? "status" : "alert"} className={`text-sm ${couponResult.valid ? "text-emerald-700 dark:text-emerald-300" : "text-destructive"}`}>
|
||
{couponResult.message}
|
||
</p>
|
||
) : null}
|
||
<dl className="space-y-2 rounded-lg border border-border/70 bg-background/60 p-3 text-sm">
|
||
<div className="flex items-center justify-between gap-3"><dt className="text-muted-foreground">قیمت اولیه</dt><dd>{money(event?.price)}</dd></div>
|
||
{couponResult?.valid && couponResult.discountAmount > 0 ? <div className="flex items-center justify-between gap-3"><dt className="text-muted-foreground">تخفیف</dt><dd>− {formatNumberPersian(couponResult.discountAmount)} تومان</dd></div> : null}
|
||
<div className="flex items-center justify-between gap-3 border-t border-border/70 pt-2 font-semibold"><dt>قیمت نهایی</dt><dd>{formatNumberPersian(getFinalPriceToman(event?.price ?? 0, couponResult?.valid ? couponResult.finalPrice : null))} تومان</dd></div>
|
||
</dl>
|
||
</section>
|
||
) : null}
|
||
{actionError ? <p role="alert" className="text-destructive">{actionError}</p> : null}
|
||
{refreshError ? <p role="status" className="text-muted-foreground">آخرین وضعیت ذخیرهشده نمایش داده شده است؛ دریافت بهروزرسانی با خطا روبهرو شد.</p> : null}
|
||
{data?.registration?.cancellation_reason === "checkout_expired" ? <p>ظرفیت رزرو قبلی آزاد شده است.</p> : null}
|
||
{data?.payment?.refund_status === "manual_review" ? <p>بازگشت وجه هنوز از درگاه تأیید نشده است.</p> : null}
|
||
{state === "free_success" && data?.event.registration_success_markdown ? (
|
||
<div>
|
||
<p className="mb-1 font-semibold">پیام برگزارکننده</p>
|
||
<Markdown content={data?.event.registration_success_markdown ?? ""} justify size="base" />
|
||
</div>
|
||
) : null}
|
||
</OutcomePage>
|
||
</>
|
||
);
|
||
}
|