This commit is contained in:
Rbiter 2025-06-10 18:21:58 +03:00
parent 3c843d73e7
commit a3a0db7dcd
21 changed files with 782 additions and 99 deletions

Binary file not shown.

212
backend/api/v0/payments.py Normal file
View File

@ -0,0 +1,212 @@
from fastapi import APIRouter, Depends, HTTPException, Body, Header, Request
from sqlalchemy.orm import Session
from core.db import get_db, Sale, User, Poster
from core.pay import YooKassaPayment
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import json
class PaymentRequest(BaseModel):
amount: float
currency: str
description: str
return_url: str
user_id: Optional[int] = Field(None) # Делаем необязательным
poster_id: Optional[int] = None
router = APIRouter()
# Конфигурация YooKassa
SHOP_ID = "1017909"
API_KEY = "test_udCBy7nXqGavVoj3RrzIRXN9UL02_UnF0FBBykxWH60"
payment_service = YooKassaPayment(SHOP_ID, API_KEY)
@router.post("/create-payment/")
async def create_payment(
payment_data: dict = Body(...), # Принимаем любой JSON
db: Session = Depends(get_db)
):
try:
# Логирование для отладки
print(f"Received payment data: {json.dumps(payment_data, ensure_ascii=False)}")
# Создание платежа через YooKassa
payment_response = payment_service.create_payment(
amount=float(payment_data.get("amount", 0)),
currency=payment_data.get("currency", "RUB"),
description=payment_data.get("description", ""),
return_url=payment_data.get("return_url", "http://localhost:3000/payments")
)
# Логирование ответа YooKassa
print(f"YooKassa response: {json.dumps(payment_response, ensure_ascii=False)}")
# Сохранение данных о платеже в БД
new_sale = Sale(
amount=float(payment_data.get("amount", 0)),
status="pending",
description=payment_data.get("description", ""),
poster_id=payment_data.get("poster_id"),
user_id=payment_data.get("user_id", 1), # Используем 1 как значение по умолчанию
user_email=payment_data.get("user_email", "example@example.com"),
payment_id=payment_response["id"],
confirmation_url=payment_response["confirmation"]["confirmation_url"]
)
db.add(new_sale)
db.commit()
db.refresh(new_sale)
return {"confirmation_url": new_sale.confirmation_url, "payment_id": payment_response["id"]}
except Exception as e:
print(f"ERROR creating payment: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
@router.get("/payment-status/{payment_id}")
async def get_payment_status(payment_id: str, db: Session = Depends(get_db)):
try:
print(f"Checking payment status for ID: {payment_id}")
# Получение информации о платеже из БД
sale = db.query(Sale).filter(Sale.payment_id == payment_id).first()
if not sale:
raise HTTPException(status_code=404, detail=f"Платеж с ID {payment_id} не найден")
# Обновление статуса из YooKassa
yookassa_status = payment_service.get_payment_status(payment_id)
if yookassa_status and "status" in yookassa_status:
# Маппинг статусов YooKassa на наши статусы
if yookassa_status["status"] == "succeeded" or yookassa_status["paid"] == True:
sale.status = "paid"
elif yookassa_status["status"] == "canceled":
sale.status = "canceled"
elif yookassa_status["status"] == "waiting_for_capture":
sale.status = "waiting_for_capture"
# Сохраняем обновленный статус
db.commit()
print(f"Updated payment status from YooKassa: {yookassa_status['status']} -> {sale.status}")
return {"payment_id": sale.payment_id, "status": sale.status}
except Exception as e:
print(f"ERROR checking payment status: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/notifications/")
async def process_notification(request: Request, db: Session = Depends(get_db)):
try:
# Получаем тело запроса
notification_data = await request.json()
print(f"Received notification: {json.dumps(notification_data, ensure_ascii=False)}")
# Проверяем тип события
event = notification_data.get("event")
payment_id = notification_data.get("object", {}).get("id")
if not payment_id:
raise HTTPException(status_code=400, detail="Missing payment ID")
# Находим платеж в БД
sale = db.query(Sale).filter(Sale.payment_id == payment_id).first()
if not sale:
raise HTTPException(status_code=404, detail=f"Payment {payment_id} not found")
# Обновляем статус платежа в зависимости от события
if event == "payment.waiting_for_capture":
sale.status = "waiting_for_capture"
elif event == "payment.succeeded":
sale.status = "paid"
elif event == "payment.canceled":
sale.status = "canceled"
else:
print(f"Unknown event: {event}")
db.commit()
return {"success": True}
except Exception as e:
print(f"ERROR processing notification: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/update-payment-status/")
async def manual_update_payment_status(payment_id: str, new_status: str, db: Session = Depends(get_db)):
try:
sale = db.query(Sale).filter(Sale.payment_id == payment_id).first()
if not sale:
raise HTTPException(status_code=404, detail="Платеж не найден")
# Обновляем статус
sale.status = new_status
db.commit()
return {"message": "Статус платежа обновлен", "payment_id": sale.payment_id, "status": sale.status}
except Exception as e:
print(f"ERROR updating payment status: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/user-tickets/{user_id}")
async def get_user_tickets(user_id: int, db: Session = Depends(get_db)):
"""Получение всех оплаченных билетов пользователя"""
try:
# Проверяем существование пользователя
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Получаем все успешно оплаченные билеты пользователя
sales = db.query(Sale).filter(Sale.user_id == user_id, Sale.status == "paid").all()
result = []
for sale in sales:
# Получаем информацию о мероприятии
poster = None
if sale.poster_id:
poster = db.query(Poster).filter(Poster.id == sale.poster_id).first()
# Формирование даты покупки
purchase_date = sale.date if hasattr(sale, 'date') else datetime.now().isoformat()
ticket_data = {
"id": sale.id,
"amount": sale.amount,
"description": sale.description,
"payment_id": sale.payment_id,
"purchase_date": purchase_date,
"poster": None
}
if poster:
# Используем только те поля, которые точно есть в модели Poster
ticket_data["poster"] = {
"id": poster.id,
"title": poster.title,
"date": poster.date
# Убрали поле img, которого нет в модели
}
result.append(ticket_data)
return result
except HTTPException as he:
# Пробрасываем дальше HTTPException
raise he
except Exception as e:
print(f"ERROR getting user tickets: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# Для тестирования, также добавим временный эндпоинт для обновления статуса билета
@router.post("/update-ticket-status/{sale_id}")
async def update_ticket_status(sale_id: int, status: str, db: Session = Depends(get_db)):
"""Временный эндпоинт для обновления статуса билета (для тестирования)"""
try:
sale = db.query(Sale).filter(Sale.id == sale_id).first()
if not sale:
raise HTTPException(status_code=404, detail="Билет не найден")
sale.status = status
db.commit()
return {"success": True, "message": f"Статус билета с ID {sale_id} обновлен на {status}"}
except Exception as e:
print(f"ERROR updating ticket status: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))

Binary file not shown.

View File

@ -3,7 +3,7 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
DATABASE_URL = "sqlite:///c:/Users/rbiter/Desktop/AboutMe/backend/database.db"
DATABASE_URL = "sqlite:///./database.db"
engine = create_engine(DATABASE_URL)

52
backend/core/pay.py Normal file
View File

@ -0,0 +1,52 @@
import uuid
import requests
import base64
class YooKassaPayment:
def __init__(self, shop_id: str, api_key: str):
self.shop_id = shop_id
self.api_key = api_key
self.auth_token = base64.b64encode(f'{shop_id}:{api_key}'.encode()).decode()
def create_payment(self, amount: float, currency: str, description: str, return_url: str, order_id: str = None):
idempotence_key = str(uuid.uuid4())
headers = {
'Content-Type': 'application/json',
'Authorization': f'Basic {self.auth_token}',
'Idempotence-Key': idempotence_key
}
data = {
"amount": {
"value": f"{amount:.2f}",
"currency": currency
},
"confirmation": {
"type": "redirect",
"return_url": return_url
},
"capture": True,
"description": description,
"metadata": {
"order_id": order_id or str(uuid.uuid4())
}
}
response = requests.post("https://api.yookassa.ru/v3/payments", json=data, headers=headers)
if response.status_code in [200, 201]:
return response.json()
else:
raise Exception(f"Ошибка {response.status_code}: {response.text}")
def get_payment_status(self, payment_id: str):
"""Получение актуального статуса платежа из YooKassa API"""
headers = {
'Content-Type': 'application/json',
'Authorization': f'Basic {self.auth_token}'
}
response = requests.get(f"https://api.yookassa.ru/v3/payments/{payment_id}", headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"Error getting payment status: {response.status_code} {response.text}")
return None

View File

@ -0,0 +1,24 @@
from pydantic import BaseModel, constr, condecimal
from typing import Optional
class PaymentCreateSchema(BaseModel):
amount: condecimal(gt=0) # Amount must be greater than 0
currency: constr(min_length=3, max_length=3) # Currency code must be 3 characters
description: str
return_url: str
capture: bool = True
metadata: Optional[dict] = None
class PaymentResponseSchema(BaseModel):
id: str
status: str
confirmation_url: str
class PaymentStatusSchema(BaseModel):
id: str
status: str
amount: condecimal(gt=0)
currency: constr(min_length=3, max_length=3) # Currency code must be 3 characters
description: str
created_at: str # ISO format date string
confirmation_url: Optional[str] = None

Binary file not shown.

View File

@ -6,6 +6,7 @@ from api.v0.user import router as user_router
from fastapi.staticfiles import StaticFiles
from api.v0.poster import router as poster_router
from api.v0.feedback import router as feedback_router
from api.v0.payments import router as payment_router
db.initialize_database()
@ -15,6 +16,7 @@ app = FastAPI()
app.include_router(user_router)
app.include_router(poster_router)
app.include_router(feedback_router)
app.include_router(payment_router)
app.mount("/res", StaticFiles(directory="res"), name="res")

View File

@ -1,39 +0,0 @@
import uuid
import requests
import base64
shop_id = '1017909'
api_key = 'test_udCBy7nXqGavVoj3RrzIRXN9UL02_UnF0FBBykxWH60'
auth_token = base64.b64encode(f'{shop_id}:{api_key}'.encode()).decode()
idempotence_key = str(uuid.uuid4())
headers = {
'Content-Type': 'application/json',
'Authorization': f'Basic {auth_token}',
'Idempotence-Key': idempotence_key
}
data = {
"amount": {
"value": "1500.00",
"currency": "RUB"
},
"confirmation": {
"type": "redirect",
"return_url": "https://example.com/return"
},
"capture": True,
"description": "Зеленый слоник 2",
"metadata": {
"order_id": str(uuid.uuid4())
}
}
response = requests.post("https://api.yookassa.ru/v3/payments", json=data, headers=headers)
if response.status_code in [200, 201]:
print(response.json()["confirmation"]["confirmation_url"])
else:
print(f"Ошибка {response.status_code}: {response.text}")

7
backend/requirements.txt Normal file
View File

@ -0,0 +1,7 @@
fastapi
uvicorn
sqlalchemy
pydantic
python-multipart
PyJWT
requests

View File

@ -0,0 +1,172 @@
'use client';
import React, { useEffect, useState, CSSProperties } from 'react';
import Header from '@/components/Header';
import Fotter from '@/components/Fotter';
import { useSearchParams, useRouter } from 'next/navigation';
const styles = `
.page-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background: radial-gradient(circle at top right, rgba(30,30,30,0.8) 0%, rgba(15,15,15,0.8) 100%);
position: relative;
overflow-x: hidden;
}
.page-container::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('/noise-texture.png'), linear-gradient(145deg, rgba(255,127,39,0.1) 0%, rgba(0,0,0,0) 70%);
opacity: 0.4;
pointer-events: none;
z-index: -1;
}
.content-container {
flex: 1;
animation: fadeInUp 0.6s ease-out forwards;
width: 100%;
}
.section-title {
color: #ff7f27;
font-size: 2.5rem;
margin-bottom: 1.5rem;
font-weight: bold;
background: linear-gradient(to right, #ff7f27, #ff5500);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: glowPulse 3s infinite;
text-align: center;
letter-spacing: -0.5px;
}
.payment-status {
text-align: center;
margin: 2rem auto;
padding: 2rem;
border-radius: 1rem;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,127,39,0.2);
color: rgba(255,255,255,0.9);
font-size: 1.2rem;
}
.success-message {
color: #30d158;
}
.error-message {
color: #ff453a;
}
.loading-message {
color: rgba(255,255,255,0.7);
}
`;
const mainContentStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
maxWidth: '90%',
margin: '0 auto',
flex: 1,
paddingTop: '76px',
paddingBottom: '30px',
};
export default function PaymentStatusPage() {
const searchParams = useSearchParams();
const router = useRouter();
const paymentId = searchParams.get('payment_id');
const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const checkPaymentStatus = async () => {
// Пытаемся получить payment_id из разных источников
const urlPaymentId = searchParams.get('payment_id');
const returnPaymentId = searchParams.get('return_payment_id');
const storedPaymentId = typeof window !== 'undefined' ? localStorage.getItem('current_payment_id') : null;
// Используем первый доступный ID
const finalPaymentId = urlPaymentId || returnPaymentId || storedPaymentId;
console.log("Available payment IDs:", { urlPaymentId, returnPaymentId, storedPaymentId });
if (!finalPaymentId) {
setStatus('error');
setError('ID платежа не найден');
setLoading(false);
return;
}
try {
console.log("Checking payment status for:", finalPaymentId);
const response = await fetch(`http://localhost:8000/payment-status/${finalPaymentId}`);
if (!response.ok) {
throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log("Payment status response:", data);
setStatus(data.status);
// Если платеж завершен, перенаправляем через 5 секунд
if (data.status === 'paid') {
setTimeout(() => {
router.push('/'); // или на страницу с билетами пользователя
}, 5000);
}
} catch (error) {
console.error("Error fetching payment status:", error);
setError('Не удалось получить информацию о платеже');
setStatus('error');
} finally {
setLoading(false);
}
};
checkPaymentStatus();
// Проверяем статус каждые 5 секунд, если платеж ожидает оплаты
const intervalId = setInterval(() => {
if (status === 'pending' || status === 'waiting_for_capture') {
checkPaymentStatus();
}
}, 5000);
return () => clearInterval(intervalId);
}, [paymentId, router, status]);
return (
<div className="page-container">
<style>{styles}</style>
<Header />
<main style={mainContentStyle} className="content-container">
<h1 className="section-title">Статус платежа</h1>
{loading ? (
<div className="payment-status loading-message">Загрузка статуса платежа...</div>
) : status === 'paid' ? (
<div className="payment-status success-message">Ваш платеж успешно завершен!</div>
) : status === 'pending' ? (
<div className="payment-status">Ваш платеж находится в ожидании подтверждения.</div>
) : status === 'failed' ? (
<div className="payment-status error-message">Ваш платеж не удалось завершить.</div>
) : (
<div className="payment-status error-message">Произошла ошибка. Платеж не найден.</div>
)}
</main>
<Fotter />
</div>
);
}

View File

@ -21,6 +21,7 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps)
const [liked, setLiked] = useState(false);
const [likesCount, setLikesCount] = useState(data.likes);
const [loading, setLoading] = useState(false);
const [paymentLoading, setPaymentLoading] = useState(false);
const [message, setMessage] = useState<{text: string, type: 'success' | 'error' | 'info' | null}>({
text: '',
type: null
@ -125,6 +126,93 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps)
}
}, [data.id, liked, loading]);
// Функция для обработки платежа
const handlePayment = useCallback(async () => {
if (paymentLoading) return;
try {
setPaymentLoading(true);
// Получаем JWT токен из cookie
const token = document.cookie
.split('; ')
.find(row => row.startsWith('JWT_token='))
?.split('=')[1];
if (!token) {
setMessage({
text: 'Необходимо авторизоваться для покупки билета',
type: 'error'
});
setTimeout(() => setMessage({text: '', type: null}), 3000);
return;
}
try {
// Получаем данные пользователя из JWT
const payload = JSON.parse(atob(token.split('.')[1]));
console.log("JWT payload:", payload);
// ID пользователя может храниться в разных полях JWT
const userId = payload.sub || payload.user_id || payload.id || 1;
console.log("User ID:", userId);
// Создание запроса на оплату
const response = await fetch('http://localhost:8000/create-payment/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
amount: data.price,
currency: 'RUB',
description: `Билет на мероприятие: ${data.title}`,
return_url: `${window.location.origin}/payments`,
user_id: userId,
poster_id: data.id
})
});
console.log("Payment response status:", response.status);
const responseData = await response.json();
console.log("Payment response data:", responseData);
if (response.ok && responseData.confirmation_url) {
// Сохраняем payment_id в localStorage перед перенаправлением
if (responseData.payment_id) {
localStorage.setItem('current_payment_id', responseData.payment_id);
}
// Добавляем payment_id в URL вручную
window.location.href = `${responseData.confirmation_url}&return_payment_id=${responseData.payment_id}`;
} else {
setMessage({
text: responseData.detail || 'Произошла ошибка при создании платежа',
type: 'error'
});
setTimeout(() => setMessage({text: '', type: null}), 3000);
}
} catch (parseError) {
console.error('Ошибка при обработке JWT:', parseError);
setMessage({
text: 'Ошибка авторизации. Пожалуйста, войдите заново.',
type: 'error'
});
setTimeout(() => setMessage({text: '', type: null}), 3000);
}
} catch (error) {
console.error('Ошибка при создании платежа:', error);
setMessage({
text: 'Не удалось создать платёж. Попробуйте позже.',
type: 'error'
});
setTimeout(() => setMessage({text: '', type: null}), 3000);
} finally {
setPaymentLoading(false);
}
}, [data.id, data.price, data.title, paymentLoading]);
if (!isOpen) return null;
// Расширенные стили с усиленными эффектами размытия и поддержкой кросс-браузерности
@ -416,22 +504,48 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps)
border: 0,
borderRadius: 24,
fontWeight: 'bold',
cursor: 'pointer',
cursor: paymentLoading ? 'wait' : 'pointer',
color: '#000',
transition: 'all 0.3s',
boxShadow: '0 2px 10px rgba(255,127,39,0.4)'
boxShadow: '0 2px 10px rgba(255,127,39,0.4)',
display: 'flex',
alignItems: 'center',
gap: 8,
opacity: paymentLoading ? 0.8 : 1
}}
onClick={() => alert('Функция покупки билетов будет доступна в ближайшее время!')}
onClick={handlePayment}
disabled={paymentLoading}
onMouseOver={(e) => {
if (!paymentLoading) {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 15px rgba(255,127,39,0.5)';
}
}}
onMouseOut={(e) => {
if (!paymentLoading) {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 10px rgba(255,127,39,0.4)';
}
}}
>
Купить билет
{paymentLoading ? (
<>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="loading-spinner"
>
<circle cx="12" cy="12" r="10" stroke="rgba(0,0,0,0.3)" strokeWidth="2.5" />
<path d="M12 2C6.47715 2 2 6.47715 2 12" stroke="#000" strokeWidth="2.5" />
</svg>
Обработка...
</>
) : (
'Купить билет'
)}
</button>
</div>
</div>

View File

@ -1,7 +1,71 @@
'use client';
import { CSSProperties, useState, useEffect } from 'react';
// Улучшенные стили с градиентами и эффектами
// Обновите стили CSS для прокрутки
const styles = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes profileCardAppear {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Стилизация скроллбара */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
margin: 5px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(145deg, #ff7f27, #e96c00);
border-radius: 10px;
border: 2px solid rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(145deg, #ff9540, #ff7f27);
}
::-webkit-scrollbar-corner {
background: transparent;
}
.loading-spinner {
animation: spin 1s linear infinite;
}
.tab-content-scroll {
max-height: calc(65vh - 280px); /* Динамическая высота на основе высоты окна */
overflow-y: auto;
padding-right: 10px; /* Увеличиваем отступ для скроллбара */
padding-bottom: 20px; /* Добавляем отступ снизу */
-webkit-overflow-scrolling: touch; /* Плавная прокрутка на iOS */
}
.ticket-card {
animation: fadeIn 0.5s forwards;
}
.ticket-card:last-child {
margin-bottom: 20px; /* Дополнительный отступ для последнего билета */
}
`;
// Существующие стили...
const wrapper: CSSProperties = {
position: 'fixed',
inset: 0,
@ -20,15 +84,18 @@ const modal: CSSProperties = {
background: 'linear-gradient(145deg, #0a0a0a, #1a1a1a)',
border: '1px solid #ff7f27',
borderRadius: 24,
padding: 40,
minWidth: 700, // Увеличено для более широкого профиля
padding: '40px 40px 20px 40px', // Уменьшаем нижний отступ
minWidth: 700,
maxWidth: '90vw',
width: '100%', // Добавлено для лучшего контроля ширины
width: '100%',
color: '#ff7f27',
boxShadow: '0 8px 40px rgba(255,127,39,.25), 0 0 0 1px rgba(255,127,39,0.1)',
transformOrigin: 'center',
transition: 'transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
animation: 'profileCardAppear 0.4s forwards',
maxHeight: '85vh',
display: 'flex',
flexDirection: 'column',
};
const profileHeader: CSSProperties = {
@ -125,9 +192,14 @@ const tabActive: CSSProperties = {
background: 'rgba(255,127,39,0.1)',
};
// Обновленный стиль для контента вкладок
const tabContent: CSSProperties = {
minHeight: 200,
padding: '20px 0',
padding: '20px 0 0 0', // Убираем нижний padding
flex: 1,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
};
const settingsButton: CSSProperties = {
@ -254,6 +326,20 @@ interface UserData {
full_name?: string; // Для обратной совместимости
}
interface Ticket {
id: number;
amount: number;
description: string;
payment_id: string;
purchase_date: string;
poster: {
id: number;
title: string;
date: string;
img: string;
} | null;
}
export default function ProfileModal({
user,
onClose,
@ -265,7 +351,8 @@ export default function ProfileModal({
}) {
const [activeTab, setActiveTab] = useState('tickets');
const [userData, setUserData] = useState<UserData | null>(null);
// Removed unused loading state
const [tickets, setTickets] = useState<Ticket[]>([]);
const [ticketsLoading, setTicketsLoading] = useState(true);
// Получение ID пользователя из cookie
const userId = document.cookie
@ -273,14 +360,14 @@ export default function ProfileModal({
.find(row => row.startsWith('id='))
?.split('=')[1];
// Получение токена из cookie (removed unused token variable)
// Загрузка полных данных пользователя при открытии профиля
// Загрузка данных пользователя и билетов
useEffect(() => {
if (userId) {
fetchUserData(userId);
fetchUserTickets(userId);
} else {
setLoading(false);
setTicketsLoading(false);
}
}, [userId]);
@ -309,6 +396,28 @@ export default function ProfileModal({
}
};
// Новая функция для загрузки билетов
const fetchUserTickets = async (id: string) => {
try {
const response = await fetch(`http://localhost:8000/user-tickets/${id}`, {
method: 'GET',
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
console.log("Получены билеты пользователя:", data);
setTickets(data);
} else {
console.error("Ошибка при получении билетов");
}
} catch (error) {
console.error("Ошибка запроса билетов:", error);
} finally {
setTicketsLoading(false);
}
};
// Функция форматирования полного имени
const formatName = () => {
if (userData) {
@ -328,6 +437,8 @@ export default function ProfileModal({
style={wrapper}
onClick={(e) => e.target === e.currentTarget && onClose()}
>
{/* Добавляем тег style с нашими стилями */}
<style>{styles}</style>
<div style={modal}>
<button
@ -412,50 +523,77 @@ export default function ProfileModal({
</div>
{/* Содержимое вкладок */}
<div style={tabContent} className={`tab-content-active`}>
<div style={tabContent}>
{activeTab === 'tickets' && (
<div>
<div style={ticketCard}>
<div className="tab-content-scroll">
{ticketsLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '30px 0' }}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
className="loading-spinner">
<circle cx="12" cy="12" r="10" stroke="rgba(255,127,39,0.3)" strokeWidth="2" />
<path d="M12 2C6.47715 2 2 6.47715 2 12" stroke="#ff7f27" strokeWidth="2" />
</svg>
</div>
) : tickets.length > 0 ? (
tickets.map(ticket => (
<div key={ticket.id} style={ticketCard} className="ticket-card">
<div style={ticketHeader}>
<div>
<div style={ticketTitle}>Зеленый слоник 2</div>
<div style={ticketTitle}>
{ticket.poster?.title || ticket.description}
</div>
</div>
<div style={ticketNumber}>
{`T${ticket.id}${ticket.payment_id.substring(0, 5).toUpperCase()}`}
</div>
<div style={ticketNumber}>F22SB21</div>
</div>
<div style={ticketDetails}>
{ticket.poster?.date && (
<div style={ticketDetail}>
<span style={ticketLabel}>ДАТА</span>
<span style={ticketValue}>21.05.2025</span>
<span style={ticketValue}>
{new Date(ticket.poster.date).toLocaleDateString('ru-RU')}
</span>
</div>
)}
<div style={ticketDetail}>
<span style={ticketLabel}>ДАТА ПОКУПКИ</span>
<span style={ticketValue}>
{new Date(ticket.purchase_date).toLocaleDateString('ru-RU')}
</span>
</div>
<div style={ticketDetail}>
<span style={ticketLabel}>ВРЕМЯ</span>
<span style={ticketValue}>19:30</span>
</div>
<div style={ticketDetail}>
<span style={ticketLabel}>ЗАЛ</span>
<span style={ticketValue}>Главный</span>
</div>
<div style={ticketDetail}>
<span style={ticketLabel}>МЕСТО</span>
<span style={ticketValue}>VIP 12</span>
<span style={ticketLabel}>СТОИМОСТЬ</span>
<span style={ticketValue}>
{ticket.amount.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB' })}
</span>
</div>
</div>
<div style={ticketPaymentId}>
<span style={{color: 'rgba(255,127,39,0.8)', marginRight: 5}}>Код платежа:</span>
2fbd2cd4-000f-5001-9000-1fb8d8bcd8a9
{ticket.payment_id}
</div>
</div>
))
) : (
<div style={emptyState}>
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ marginBottom: 16, opacity: 0.5 }}>
<path d="M20 7V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V3C4 2.44772 4.44772 2 5 2H15L20 7Z" stroke="#ff7f27" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M9 17H15M9 13H15M9 9H10" stroke="#ff7f27" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<h3 style={{ color: 'rgba(255,255,255,0.8)', marginBottom: 8 }}>Билеты отсутствуют</h3>
<p style={{ maxWidth: '70%', margin: '0 auto' }}>У вас пока нет приобретенных билетов</p>
</div>
)}
</div>
)}
{activeTab === 'history' && (
<div className="tab-content-scroll">
<div style={emptyState}>
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ marginBottom: 16, opacity: 0.5 }}>
<path d="M12 8V12L15 15M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#ff7f27" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
@ -463,6 +601,7 @@ export default function ProfileModal({
<h3 style={{ color: 'rgba(255,255,255,0.8)', marginBottom: 8 }}>История заказов пуста</h3>
<p style={{ maxWidth: '70%', margin: '0 auto' }}>Здесь появится история ваших прошлых заказов</p>
</div>
</div>
)}
</div>

View File