'use client' import { useCallback, useEffect, useState } from 'react' import { texts, format } from '@/texts' import type { DiscountBearer, DiscountCode, DiscountRedemption, DiscountReport, DiscountType } from '@/services/discountCodes' import { Chip } from '@/components/heroui/Chip' import { addToast } from '@/lib/toast' import Button from '@/components/formElements/Button' import Input from '@/components/formElements/Input' import StatusChip from '@/components/ui/StatusChip' import { getActiveStatus } from '@/constants/status' import { convertPersianToEnglish, formatCurrency, formatNumber, coerceToString } from '@/helpers' import { formatPersianDate } from '@/lib/formatters' import { BULK_CREATE_DISCOUNT_CODES, DELETE_DISCOUNT_CODE, GET_DISCOUNT_MANAGEMENT_BOOTSTRAP, SET_DISCOUNT_CODE_ACTIVE, } from '@/services/discountCodes' interface EventDiscountsPanelProps { eventId: string isFree: boolean isEnded?: boolean /** Admins can shift the discount cost onto the platform. Hosts always absorb it. */ isAdmin?: boolean } const number = formatNumber const toInt = (value: string): number | undefined => { const parsed = Number(convertPersianToEnglish(value).replace(/[^\d]/g, '')) return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined } const describeCode = (code: DiscountCode): string => code.type === 'percent' ? format(texts.events.percentOff, { value: number(code.value) }) : format(texts.events.amountOff, { value: formatCurrency(code.value) }) const DISCOUNT_TYPE_OPTIONS = [ { code: 'percent', name: texts.events.discountTypePercent }, { code: 'fixed', name: texts.events.discountTypeFixed }, ] as const const DISCOUNT_BEARER_OPTIONS = [ { code: 'organizer', name: texts.events.host }, { code: 'platform', name: texts.events.bearerPlatform }, ] as const const EventDiscountsPanel = ({ eventId, isFree, isEnded = false, isAdmin = false }: EventDiscountsPanelProps) => { const [codes, setCodes] = useState([]) const [redemptions, setRedemptions] = useState([]) const [report, setReport] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) // Create form const [type, setType] = useState('percent') const [value, setValue] = useState('') const [quantity, setQuantity] = useState('1') const [maxUses, setMaxUses] = useState('') const [maxUsesPerUser, setMaxUsesPerUser] = useState('') const [bearer, setBearer] = useState('organizer') const [showAdvanced, setShowAdvanced] = useState(false) const [isCreating, setIsCreating] = useState(false) const [pendingCodeId, setPendingCodeId] = useState(null) const [lastBatch, setLastBatch] = useState(null) const load = useCallback(async () => { setIsLoading(true) setError(null) const result = await GET_DISCOUNT_MANAGEMENT_BOOTSTRAP(eventId) if (!result.ok) { setError(texts.events.discountsLoadFailed) setIsLoading(false) return } setCodes(result.data.codes.items) setReport(result.data.report) setRedemptions(result.data.redemptions.items) setIsLoading(false) }, [eventId]) useEffect(() => { if (!isFree) void load() else setIsLoading(false) }, [isFree, load]) const canCreate = !isFree && !isEnded const canMutateCodes = !isEnded const handleCreate = async () => { const numericValue = toInt(value) const numericQuantity = toInt(quantity) if (!numericValue) { addToast({ title: texts.events.discountValueRequired, color: 'warning' }) return } if (type === 'percent' && (numericValue < 1 || numericValue > 99)) { addToast({ title: texts.events.discountPercentRange, color: 'warning' }) return } if (!numericQuantity) { addToast({ title: texts.events.discountCountRequired, color: 'warning' }) return } setIsCreating(true) const result = await BULK_CREATE_DISCOUNT_CODES(eventId, { type, value: numericValue, quantity: numericQuantity, maxUses: toInt(maxUses), maxUsesPerUser: toInt(maxUsesPerUser), bearer: isAdmin ? bearer : undefined, }) setIsCreating(false) if (!result.ok) { addToast({ title: texts.events.discountCreateFailed, description: result.error.message, color: 'danger' }) return } addToast({ title: format(texts.events.discountCreatedCount, { count: number(result.data.count) }), color: 'success' }) setLastBatch(result.data.codes) setValue('') setQuantity('1') setMaxUses('') setMaxUsesPerUser('') await load() } const handleToggleActive = async (code: DiscountCode, nextActive: boolean) => { if (!canMutateCodes) { addToast({ title: texts.events.discountToggleAfterEnd, color: 'warning' }) return } setPendingCodeId(code.id) const result = await SET_DISCOUNT_CODE_ACTIVE(code.id, nextActive) setPendingCodeId(null) if (!result.ok) { addToast({ title: texts.events.discountToggleFailed, color: 'danger' }) return } setCodes((current) => current.map((item) => (item.id === code.id ? { ...item, isActive: nextActive } : item))) } const handleDelete = async (code: DiscountCode) => { if (!canMutateCodes) { addToast({ title: texts.events.discountDeleteAfterEnd, color: 'warning' }) return } if (code.redeemedCount > 0) { addToast({ title: texts.events.discountDeleteUsed, color: 'warning' }) return } setPendingCodeId(code.id) const result = await DELETE_DISCOUNT_CODE(code.id) setPendingCodeId(null) if (!result.ok) { addToast({ title: texts.events.discountDeleteFailed, description: result.error.message, color: 'danger' }) return } addToast({ title: texts.events.discountDeleted, color: 'success' }) setCodes((current) => current.filter((item) => item.id !== code.id)) } const copyCodes = async (list: string[]) => { try { await navigator.clipboard.writeText(list.join('\n')) addToast({ title: texts.events.codesCopied, color: 'success' }) } catch { addToast({ title: texts.events.copyFailed, color: 'danger' }) } } if (isFree) { return (

{texts.events.eventIsFreeTitle}

{texts.events.freeEventNoDiscounts}

) } return (
{report ? (
{texts.events.activeCodes} {number(report.totalCodes)}
{texts.events.redemptionCount} {number(report.totalRedemptions)}
{texts.events.totalDiscount} {formatCurrency(report.totalDiscountAmount)}
{texts.events.platformDiscountShare} {formatCurrency(report.totalPlatformAbsorbed)}
) : null}

{texts.events.createDiscountTitle}

{texts.events.createDiscountIntro}

{!canCreate ? (
{texts.events.createDiscountEnded}
) : ( <>
{ setType(coerceToString(next) as DiscountType) }} /> { setValue(coerceToString(next)) }} /> { setQuantity(coerceToString(next)) }} /> {isAdmin ? ( { setBearer(coerceToString(next) as DiscountBearer) }} /> ) : null}
{showAdvanced ? (
{ setMaxUses(coerceToString(next)) }} /> { setMaxUsesPerUser(coerceToString(next)) }} />
) : null} )} {lastBatch?.length ? (

{format(texts.events.codesCreatedCount, { count: number(lastBatch.length) })}

{lastBatch.map((code) => ( {code} ))}
) : null}

{texts.events.definedCodesTitle}

{isLoading ? (
{[0, 1, 2].map((item) => (
))}
) : error ? (
{error}
) : codes.length === 0 ? (
{texts.events.noDiscountCodes}
) : (
{codes.map((code) => (

{code.code}

{code.bearer === 'platform' ? ( {texts.events.platformCostChip} ) : null}

{describeCode(code)}

{texts.events.redemptionCount} {number(code.redeemedCount)} {code.maxUses ? ` / ${number(code.maxUses)}` : ''}
{texts.events.totalDiscount} {formatCurrency(code.totalDiscountAmount)}
{texts.events.createdLabel} {formatPersianDate(code.createdAt)}
void handleToggleActive(code, Boolean(next))} /> {isEnded ? texts.events.eventEndedShort : code.isActive ? texts.events.availableToGuests : texts.events.deactivated}
))}
)}
{!isLoading && !error && redemptions.length ? (

{format(texts.events.redeemedCodesTitle, { count: number(redemptions.length) })}

{redemptions.map((r) => (
{r.code} {r.bearer === 'platform' ? ( {texts.events.bearerPlatform} ) : null}

{formatPersianDate(r.createdAt)}

-{formatCurrency(r.discountAmount)}

{format(texts.events.payableAmount, { amount: formatCurrency(r.payableAmount) })}

))}
) : null}
) } export default EventDiscountsPanel