cyberia/research/events/client/src/features/ticket/ui/CategoryAllocation.tsx

import { eventQueries } from "@/entities/event";
import type { TicketAllocation } from "@/entities/order";
import { formatUsdt } from "@/shared/lib/money/usdt";
import { getCurrencySymbol } from "@/shared/lib/web3";
import { Loader } from "@/shared/ui/Loader";
import { Button } from "@/shared/ui/button";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo } from "react";

export interface CategoryOption {
  categoryId: number;
  name: string;
  discount: number;
  quota: number;
  hasQuota: boolean;
  ticketsBought: number;
}

interface CategoryAllocationProps {
  eventId: number;
  ticketPrice: bigint;
  allocations: TicketAllocation[];
  onAllocationsChange: (allocations: TicketAllocation[]) => void;
}

export const CategoryAllocation: React.FC<CategoryAllocationProps> = ({
  eventId,
  ticketPrice,
  allocations,
  onAllocationsChange,
}) => {
  const { data: event, isLoading: isEventLoading } = useQuery(
    eventQueries.detail(eventId),
  );
  const { data: categories, isLoading } = useQuery(
    eventQueries.categories(eventId),
  );

  const categoryMap = useMemo(() => {
    if (!categories) return new Map<number, CategoryOption>();
    return new Map(categories.map((c) => [c.categoryId, c]));
  }, [categories]);

  const getCategoryPrice = (category: CategoryOption): bigint => {
    if (category.discount === 0) return ticketPrice;
    // Contract amounts are integer token units. Keep this integer to avoid validation issues.
    const discount = (ticketPrice * BigInt(category.discount)) / 10000n;
    return ticketPrice - discount;
  };

  const getGlobalRemaining = (): number => {
    if (!event) return 0;
    return Math.max(
      0,
      event.place.maxTickets - Number(event.ticketsBought ?? 0),
    );
  };

  const isSoldOut = (category: CategoryOption | undefined): boolean => {
    if (!category) return true;
    if (getGlobalRemaining() <= 0) return true;
    if (!category.hasQuota) return false;
    return category.ticketsBought >= category.quota;
  };

  const getAllocationCount = (categoryId: number): number => {
    const allocation = allocations.find((a) => a.categoryId === categoryId);
    return allocation?.count ?? 0;
  };

  const updateCategoryCount = (categoryId: number, delta: number) => {
    const category = categoryMap.get(categoryId);
    if (!category) return;

    const currentCount = getAllocationCount(categoryId);
    const globalRemaining = getGlobalRemaining();
    const totalSelected = allocations.reduce((sum, a) => sum + a.count, 0);
    const otherSelected = totalSelected - currentCount;
    const maxByGlobal = Math.max(0, globalRemaining - otherSelected);

    const quotaRemaining = category.hasQuota
      ? Math.max(0, category.quota - category.ticketsBought)
      : null;

    const maxAllowed =
      quotaRemaining !== null
        ? Math.min(quotaRemaining, maxByGlobal)
        : maxByGlobal;

    const newCount = Math.max(0, Math.min(currentCount + delta, maxAllowed));

    const newAllocations = allocations.filter(
      (a) => a.categoryId !== categoryId,
    );

    if (newCount > 0) {
      const finalPrice = getCategoryPrice(category);
      newAllocations.push({
        categoryId: category.categoryId,
        categoryName: category.name,
        discount: category.discount,
        count: newCount,
        finalPricePerTicket: finalPrice,
      });
    }

    onAllocationsChange(newAllocations);
  };

  useEffect(() => {
    if (categories && categories.length > 0 && allocations.length === 0) {
      const firstCategory = categories[0];
      if (firstCategory && !isSoldOut(firstCategory)) {
        const finalPrice = getCategoryPrice(firstCategory);
        onAllocationsChange([
          {
            categoryId: firstCategory.categoryId,
            categoryName: firstCategory.name,
            discount: firstCategory.discount,
            count: 1,
            finalPricePerTicket: finalPrice,
          },
        ]);
      }
    }
  }, [categories]);

  if (isLoading || isEventLoading) {
    return (
      <div className="flex items-center gap-2 py-3">
        <Loader />
        <span className="text-muted-foreground">Loading categories...</span>
      </div>
    );
  }

  if (!event || !categories || categories.length === 0) {
    return null;
  }

  const globalRemaining = getGlobalRemaining();
  const totalSelected = allocations.reduce((sum, a) => sum + a.count, 0);

  return (
    <div className="space-y-2">
      {categories.map((category) => {
        const price = getCategoryPrice(category);
        const count = getAllocationCount(category.categoryId);
        const soldOut = isSoldOut(category);
        const otherSelected = totalSelected - count;
        const maxByGlobal = Math.max(0, globalRemaining - otherSelected);
        const quotaRemaining = category.hasQuota
          ? Math.max(0, category.quota - category.ticketsBought)
          : null;
        const maxAllowed =
          quotaRemaining !== null
            ? Math.min(quotaRemaining, maxByGlobal)
            : maxByGlobal;
        // "Available" in the UI means how many more can be added right now,
        // considering both global event capacity and per-category quota.
        const availableToAdd = Math.max(0, maxAllowed - count);

        return (
          <div
            key={category.categoryId}
            className="flex items-center justify-between py-3 border-b border-input last:border-b-0"
          >
            <div className="flex flex-col gap-1">
              <span className="font-medium">{category.name}</span>
              <div className="flex items-center gap-3 text-sm text-muted-foreground">
                <span>
                  {formatUsdt(price)}{" "}
                  <img
                    src={getCurrencySymbol()}
                    className="h-4 aspect-square inline"
                    alt="currency"
                  />
                </span>
                <span className={availableToAdd < 5 ? "text-red-500" : ""}>
                  {`${availableToAdd} left`}
                </span>
              </div>
            </div>

            <div className="flex items-center gap-2">
              <Button
                filling="outline"
                size="icon"
                className="h-8 w-8"
                onClick={() => updateCategoryCount(category.categoryId, 1)}
                disabled={soldOut || count >= maxAllowed}
              >
                +
              </Button>
              <span className="w-8 text-center font-medium">{count}</span>
              <Button
                filling="outline"
                size="icon"
                className="h-8 w-8"
                onClick={() => updateCategoryCount(category.categoryId, -1)}
                disabled={count <= 0}
              >
                -
              </Button>
            </div>
          </div>
        );
      })}
    </div>
  );
};

Graph