mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
FIX PR Problems
This commit is contained in:
@@ -6,7 +6,7 @@ import type {
|
||||
PaginatedResponse,
|
||||
PendingPayment,
|
||||
ManualCheckResponse,
|
||||
SavedCard,
|
||||
SavedCardsResponse,
|
||||
} from '../types';
|
||||
|
||||
export const balanceApi = {
|
||||
@@ -124,10 +124,8 @@ export const balanceApi = {
|
||||
},
|
||||
|
||||
// Get saved payment methods (cards) for recurrent payments
|
||||
getSavedCards: async (): Promise<{ cards: SavedCard[]; recurrent_enabled: boolean }> => {
|
||||
const response = await apiClient.get<{ cards: SavedCard[]; recurrent_enabled: boolean }>(
|
||||
'/cabinet/balance/saved-cards',
|
||||
);
|
||||
getSavedCards: async (): Promise<SavedCardsResponse> => {
|
||||
const response = await apiClient.get<SavedCardsResponse>('/cabinet/balance/saved-cards');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -33,6 +33,30 @@ export const BackIcon = ({ className }: IconProps) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ArrowLeftIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
className={cn('h-5 w-5', className)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChevronRightIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
className={cn('h-5 w-5', className)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MenuIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
className={cn('h-5 w-5', className)}
|
||||
|
||||
@@ -430,9 +430,10 @@ export default function AdminUserDetail() {
|
||||
|
||||
const handleUpdateSubscription = async (overrideAction?: string) => {
|
||||
if (!userId) return;
|
||||
const action = overrideAction || subAction;
|
||||
if ((action === 'extend' || action === 'shorten') && toNumber(subDays, 0) <= 0) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const action = overrideAction || subAction;
|
||||
const data: UpdateSubscriptionRequest = {
|
||||
action: action as UpdateSubscriptionRequest['action'],
|
||||
...(action === 'extend' || action === 'shorten' ? { days: toNumber(subDays, 30) } : {}),
|
||||
|
||||
@@ -13,21 +13,9 @@ import type { PaginatedResponse, Transaction } from '../types';
|
||||
|
||||
import { Card } from '@/components/data-display/Card';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { ChevronDownIcon, ChevronRightIcon } from '@/components/icons';
|
||||
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
|
||||
|
||||
// Icons
|
||||
const ChevronDownIcon = ({ className = 'h-5 w-5' }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ChevronRightIcon = ({ className = 'h-5 w-5' }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const WalletIcon = ({ className = 'h-8 w-8' }: { className?: string }) => (
|
||||
<svg
|
||||
className={className}
|
||||
@@ -274,7 +262,7 @@ export default function Balance() {
|
||||
|
||||
{/* Payment Methods */}
|
||||
{paymentMethods && paymentMethods.length > 0 && (
|
||||
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<motion.div variants={staggerItem}>
|
||||
<Card>
|
||||
<h2 className="mb-4 text-lg font-semibold text-dark-100">
|
||||
{t('balance.topUpBalance')}
|
||||
@@ -435,7 +423,7 @@ export default function Balance() {
|
||||
|
||||
{/* Saved Cards Navigation */}
|
||||
{savedCardsData?.recurrent_enabled && (
|
||||
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<motion.div variants={staggerItem}>
|
||||
<Card interactive onClick={() => navigate('/balance/saved-cards')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -164,6 +164,55 @@ export default function Referral() {
|
||||
},
|
||||
});
|
||||
|
||||
const programTerms = useMemo(() => {
|
||||
if (!terms) return null;
|
||||
const showNewUserBonus = terms.first_topup_bonus_kopeks > 0;
|
||||
const showInviterBonus = terms.inviter_bonus_kopeks > 0;
|
||||
const cardCount = 2 + (showNewUserBonus ? 1 : 0) + (showInviterBonus ? 1 : 0);
|
||||
const gridColsMap: Record<number, string> = {
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-3',
|
||||
4: 'md:grid-cols-4',
|
||||
};
|
||||
const gridCols = gridColsMap[cardCount] ?? 'md:grid-cols-4';
|
||||
|
||||
return (
|
||||
<div className="bento-card">
|
||||
<h2 className="mb-4 text-lg font-semibold text-dark-100">{t('referral.terms.title')}</h2>
|
||||
<div className={`grid grid-cols-2 gap-4 ${gridCols}`}>
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.commission')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-dark-100">
|
||||
{terms.commission_percent}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.minTopup')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-dark-100">
|
||||
{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}
|
||||
</div>
|
||||
</div>
|
||||
{showNewUserBonus && (
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.newUserBonus')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-success-400">
|
||||
{formatPositive(terms.first_topup_bonus_rubles)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showInviterBonus && (
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.inviterBonus')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-success-400">
|
||||
{formatPositive(terms.inviter_bonus_rubles)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [terms, t, formatAmount, formatPositive, currencySymbol]);
|
||||
|
||||
const copyLink = async () => {
|
||||
if (!referralLink) return;
|
||||
try {
|
||||
@@ -302,56 +351,7 @@ export default function Referral() {
|
||||
</div>
|
||||
|
||||
{/* Program Terms */}
|
||||
{terms &&
|
||||
(() => {
|
||||
const showNewUserBonus = terms.first_topup_bonus_kopeks > 0;
|
||||
const showInviterBonus = terms.inviter_bonus_kopeks > 0;
|
||||
const cardCount = 2 + (showNewUserBonus ? 1 : 0) + (showInviterBonus ? 1 : 0);
|
||||
const gridCols =
|
||||
cardCount <= 2
|
||||
? 'md:grid-cols-2'
|
||||
: cardCount === 3
|
||||
? 'md:grid-cols-3'
|
||||
: 'md:grid-cols-4';
|
||||
|
||||
return (
|
||||
<div className="bento-card">
|
||||
<h2 className="mb-4 text-lg font-semibold text-dark-100">
|
||||
{t('referral.terms.title')}
|
||||
</h2>
|
||||
<div className={`grid grid-cols-2 gap-4 ${gridCols}`}>
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.commission')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-dark-100">
|
||||
{terms.commission_percent}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.minTopup')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-dark-100">
|
||||
{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}
|
||||
</div>
|
||||
</div>
|
||||
{showNewUserBonus && (
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.newUserBonus')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-success-400">
|
||||
{formatPositive(terms.first_topup_bonus_rubles)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showInviterBonus && (
|
||||
<div className="rounded-xl bg-dark-800/30 p-3">
|
||||
<div className="text-sm text-dark-500">{t('referral.terms.inviterBonus')}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-success-400">
|
||||
{formatPositive(terms.inviter_bonus_rubles)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{programTerms}
|
||||
|
||||
{/* Referrals List */}
|
||||
<div className="bento-card">
|
||||
|
||||
@@ -6,24 +6,31 @@ import { motion } from 'framer-motion';
|
||||
|
||||
import { balanceApi } from '../api/balance';
|
||||
import { useToast } from '../components/Toast';
|
||||
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||
|
||||
import { Card } from '@/components/data-display/Card';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { ArrowLeftIcon } from '@/components/icons';
|
||||
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
|
||||
|
||||
const ArrowLeftIcon = ({ className = 'h-5 w-5' }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
);
|
||||
function formatCardDate(dateStr: string): string {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
return date.toLocaleDateString();
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SavedCards() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToast();
|
||||
const confirmDelete = useDestructiveConfirm();
|
||||
|
||||
const { data: savedCardsData, refetch: refetchSavedCards } = useQuery({
|
||||
const { data: savedCardsData, isLoading } = useQuery({
|
||||
queryKey: ['saved-cards'],
|
||||
queryFn: balanceApi.getSavedCards,
|
||||
});
|
||||
@@ -32,12 +39,16 @@ export default function SavedCards() {
|
||||
const [deletingCardId, setDeletingCardId] = useState<number | null>(null);
|
||||
|
||||
const handleDeleteCard = async (cardId: number) => {
|
||||
if (!confirm(t('balance.savedCards.confirmUnlink'))) return;
|
||||
if (deletingCardId !== null) return;
|
||||
const confirmed = await confirmDelete(
|
||||
t('balance.savedCards.confirmUnlink'),
|
||||
t('balance.savedCards.unlink'),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
setDeletingCardId(cardId);
|
||||
try {
|
||||
await balanceApi.deleteSavedCard(cardId);
|
||||
await refetchSavedCards();
|
||||
queryClient.invalidateQueries({ queryKey: ['saved-cards'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['saved-cards'] });
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: t('balance.savedCards.unlinkSuccess'),
|
||||
@@ -76,8 +87,33 @@ export default function SavedCards() {
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
{/* Loading state */}
|
||||
{isLoading && (
|
||||
<motion.div variants={staggerItem}>
|
||||
<Card>
|
||||
<div className="space-y-3">
|
||||
{[1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between rounded-linear border border-dark-700/30 bg-dark-800/30 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-6 w-6 animate-pulse rounded bg-dark-700" />
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-dark-700" />
|
||||
<div className="h-3 w-24 animate-pulse rounded bg-dark-700" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8 w-20 animate-pulse rounded bg-dark-700" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Cards List */}
|
||||
{savedCards && savedCards.length > 0 ? (
|
||||
{!isLoading && savedCards && savedCards.length > 0 ? (
|
||||
<motion.div variants={staggerItem}>
|
||||
<Card>
|
||||
<div className="space-y-3">
|
||||
@@ -95,7 +131,7 @@ export default function SavedCards() {
|
||||
</div>
|
||||
<div className="text-xs text-dark-500">
|
||||
{t('balance.savedCards.linkedAt', {
|
||||
date: new Date(card.created_at).toLocaleDateString(),
|
||||
date: formatCardDate(card.created_at),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +150,7 @@ export default function SavedCards() {
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
) : savedCards ? (
|
||||
) : !isLoading && savedCards ? (
|
||||
/* Empty state - only show when data loaded and empty */
|
||||
<motion.div variants={staggerItem}>
|
||||
<Card>
|
||||
|
||||
@@ -586,6 +586,11 @@ export interface SavedCard {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SavedCardsResponse {
|
||||
cards: SavedCard[];
|
||||
recurrent_enabled: boolean;
|
||||
}
|
||||
|
||||
// Ticket notifications types
|
||||
export interface TicketNotification {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user