"use client"; import { useEffect, useState, useCallback, CSSProperties } from "react"; import { Permission } from "../../auth-context"; import { adminApi } from "../../api"; import { Header } from "../../components/Header"; import { SatsDisplay } from "../../components/SatsDisplay"; import { StatusBadge } from "../../components/StatusBadge"; import { EmptyState } from "../../components/EmptyState"; import { ConfirmationButton } from "../../components/ConfirmationButton"; import { useRequireAuth } from "../../hooks/useRequireAuth"; import { useTranslation } from "../../hooks/useTranslation"; import { components } from "../../generated/api"; import { formatDateTime } from "../../utils/date"; import { formatEur } from "../../utils/exchange"; import { layoutStyles, typographyStyles, bannerStyles, tradeCardStyles } from "../../styles/shared"; type AdminExchangeResponse = components["schemas"]["AdminExchangeResponse"]; type Tab = "upcoming" | "past"; export default function AdminTradesPage() { const t = useTranslation("admin"); const tCommon = useTranslation("common"); const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.VIEW_ALL_EXCHANGES, fallbackRedirect: "/", }); const [activeTab, setActiveTab] = useState("upcoming"); const [upcomingTrades, setUpcomingTrades] = useState([]); const [pastTrades, setPastTrades] = useState([]); const [isLoadingTrades, setIsLoadingTrades] = useState(true); const [error, setError] = useState(null); // Action state - use Set to track multiple concurrent actions const [actioningIds, setActioningIds] = useState>(new Set()); const [confirmAction, setConfirmAction] = useState<{ id: string; type: "complete" | "no_show" | "cancel"; } | null>(null); // Past trades filters const [statusFilter, setStatusFilter] = useState("all"); const [userSearch, setUserSearch] = useState(""); const fetchUpcomingTrades = useCallback(async (): Promise => { try { const data = await adminApi.getUpcomingTrades(); setUpcomingTrades(data); return null; } catch (err) { console.error("Failed to fetch upcoming trades:", err); return t("trades.errors.loadUpcomingFailed"); } }, [t]); const fetchPastTrades = useCallback(async (): Promise => { try { const params: { status?: string; user_search?: string } = {}; if (statusFilter !== "all") { params.status = statusFilter; } if (userSearch.trim()) { params.user_search = userSearch.trim(); } const data = await adminApi.getPastTrades( Object.keys(params).length > 0 ? params : undefined ); setPastTrades(data); return null; } catch (err) { console.error("Failed to fetch past trades:", err); return t("trades.errors.loadPastFailed"); } }, [statusFilter, userSearch, t]); useEffect(() => { if (user && isAuthorized) { setIsLoadingTrades(true); setError(null); Promise.all([fetchUpcomingTrades(), fetchPastTrades()]) .then(([upcomingErr, pastErr]) => { // Combine errors if both failed const errors = [upcomingErr, pastErr].filter(Boolean); if (errors.length > 0) { setError(errors.join("; ")); } }) .finally(() => { setIsLoadingTrades(false); }); } }, [user, isAuthorized, fetchUpcomingTrades, fetchPastTrades]); const handleAction = async (publicId: string, action: "complete" | "no_show" | "cancel") => { // Add this trade to the set of actioning trades setActioningIds((prev) => new Set(prev).add(publicId)); setError(null); try { if (action === "complete") { await adminApi.completeTrade(publicId); } else if (action === "no_show") { await adminApi.noShowTrade(publicId); } else if (action === "cancel") { await adminApi.cancelTrade(publicId); } // Refetch trades - errors from fetch are informational, not critical const [upcomingErr, pastErr] = await Promise.all([fetchUpcomingTrades(), fetchPastTrades()]); const fetchErrors = [upcomingErr, pastErr].filter(Boolean); if (fetchErrors.length > 0) { setError(fetchErrors.join("; ")); } setConfirmAction(null); } catch (err) { setError(err instanceof Error ? err.message : t("trades.errors.actionFailed", { action })); } finally { // Remove this trade from the set of actioning trades setActioningIds((prev) => { const next = new Set(prev); next.delete(publicId); return next; }); } }; if (isLoading) { return (
{tCommon("loading")}
); } if (!isAuthorized) { return null; } const trades = activeTab === "upcoming" ? upcomingTrades : pastTrades; return (

{t("trades.title")}

{t("trades.subtitle")}

{error &&
{error}
} {/* Tabs */}
{/* Filters for Past tab */} {activeTab === "past" && (
setUserSearch(e.target.value)} style={styles.searchInput} />
)} {isLoadingTrades ? ( ) : trades.length === 0 ? ( ) : (
{trades.map((trade) => { const isBuy = trade.direction === "buy"; const isPast = new Date(trade.slot_start) <= new Date(); const canComplete = trade.status === "booked" && isPast && activeTab === "past"; return (
{formatDateTime(trade.slot_start)}
{/* User Info */}
{trade.user_email} {trade.user_contact.telegram && ( {trade.user_contact.telegram} )} {trade.user_contact.signal && ( {t("trades.tradeDetails.signal", { value: trade.user_contact.signal })} )}
{/* Trade Details */}
{isBuy ? t("trades.tradeDetails.sellBtc") : t("trades.tradeDetails.buyBtc")} {isBuy ? t("trades.tradeDetails.sendVia", { method: trade.bitcoin_transfer_method === "onchain" ? t("trades.tradeDetails.onchain") : t("trades.tradeDetails.lightning"), }) : t("trades.tradeDetails.receiveVia", { method: trade.bitcoin_transfer_method === "onchain" ? t("trades.tradeDetails.onchain") : t("trades.tradeDetails.lightning"), })} {formatEur(trade.eur_amount)}
{t("trades.tradeDetails.rate")} € {trade.agreed_price_eur.toLocaleString("es-ES", { maximumFractionDigits: 0, })} /BTC {t("trades.tradeDetails.market")} € {trade.market_price_eur.toLocaleString("es-ES", { maximumFractionDigits: 0, })}
{""}
{/* Actions */}
{canComplete && ( <> handleAction(trade.public_id, "complete")} onCancel={() => setConfirmAction(null)} onActionClick={() => setConfirmAction({ id: trade.public_id, type: "complete", }) } actionLabel={t("trades.actions.complete")} isLoading={actioningIds.has(trade.public_id)} confirmVariant="success" confirmButtonStyle={styles.successButton} actionButtonStyle={styles.successButton} /> handleAction(trade.public_id, "no_show")} onCancel={() => setConfirmAction(null)} onActionClick={() => setConfirmAction({ id: trade.public_id, type: "no_show", }) } actionLabel={t("trades.actions.noShow")} isLoading={actioningIds.has(trade.public_id)} confirmVariant="primary" actionButtonStyle={styles.warningButton} /> )} {trade.status === "booked" && ( handleAction(trade.public_id, "cancel")} onCancel={() => setConfirmAction(null)} onActionClick={() => setConfirmAction({ id: trade.public_id, type: "cancel", }) } actionLabel={t("trades.actions.cancel")} isLoading={actioningIds.has(trade.public_id)} confirmVariant="danger" confirmButtonStyle={styles.dangerButton} /> )}
); })}
)}
); } // Page-specific styles (trade card styles are shared via tradeCardStyles) const styles: Record = { content: { flex: 1, padding: "2rem", maxWidth: "900px", margin: "0 auto", width: "100%", }, tabRow: { display: "flex", gap: "0.5rem", marginBottom: "1.5rem", }, tabButton: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.9rem", fontWeight: 500, padding: "0.75rem 1.5rem", background: "rgba(255, 255, 255, 0.03)", border: "1px solid rgba(255, 255, 255, 0.08)", borderRadius: "8px", color: "rgba(255, 255, 255, 0.6)", cursor: "pointer", transition: "all 0.2s", }, tabButtonActive: { background: "rgba(167, 139, 250, 0.15)", border: "1px solid #a78bfa", color: "#a78bfa", }, filterRow: { display: "flex", gap: "0.75rem", marginBottom: "1.5rem", flexWrap: "wrap", }, filterSelect: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.5rem 1rem", background: "rgba(255, 255, 255, 0.05)", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "6px", color: "#fff", fontSize: "0.875rem", }, searchInput: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.5rem 1rem", background: "rgba(255, 255, 255, 0.05)", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "6px", color: "#fff", fontSize: "0.875rem", minWidth: "200px", }, // Admin-specific: user contact info userInfo: { display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.5rem", flexWrap: "wrap", }, userEmail: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.875rem", color: "rgba(255, 255, 255, 0.7)", }, contactBadge: { fontFamily: "'DM Mono', monospace", fontSize: "0.7rem", padding: "0.15rem 0.5rem", background: "rgba(99, 102, 241, 0.15)", border: "1px solid rgba(99, 102, 241, 0.3)", borderRadius: "4px", color: "rgba(129, 140, 248, 0.9)", }, // Admin-specific: vertical button group buttonGroup: { display: "flex", flexDirection: "column", gap: "0.5rem", alignItems: "flex-end", }, successButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.4rem 0.85rem", fontSize: "0.75rem", fontWeight: 500, background: "rgba(34, 197, 94, 0.2)", border: "1px solid rgba(34, 197, 94, 0.3)", borderRadius: "6px", color: "#4ade80", cursor: "pointer", transition: "all 0.2s", }, warningButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.4rem 0.85rem", fontSize: "0.75rem", fontWeight: 500, background: "rgba(251, 146, 60, 0.2)", border: "1px solid rgba(251, 146, 60, 0.3)", borderRadius: "6px", color: "#fb923c", cursor: "pointer", transition: "all 0.2s", }, dangerButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.4rem 0.85rem", fontSize: "0.75rem", fontWeight: 500, background: "rgba(239, 68, 68, 0.2)", border: "1px solid rgba(239, 68, 68, 0.3)", borderRadius: "6px", color: "#f87171", cursor: "pointer", transition: "all 0.2s", }, };