Перейти к содержанию

P2-020 Frontend: Личный кабинет участника

Метаданные

Поле Значение
Фаза Phase 2: Core
Статус done
Приоритет high
Связь с roadmap Roadmap - Frontend

Контекст

Бизнес-контекст

Личный кабинет участника показывает билеты на события, историю регистраций, настройки профиля. Это «домашняя страница» для обычных пользователей (не организаторов).

Технический контекст

  • Отдельный layout для участников или часть dashboard
  • Показывает регистрации пользователя (из всех организаций)
  • Настройки профиля и уведомлений

Связанные документы: - User Journeys - Journey 7 - Role Model

Цель

Реализовать личный кабинет для участников с билетами, историей и настройками.

Definition of Ready (DoR)

  • [x] Контекст понятен и описан
  • [x] Цель сформулирована
  • [x] Acceptance Criteria определены
  • [x] Технические детали проработаны
  • [x] Зависимости определены
  • [x] Нет блокеров

Acceptance Criteria

Мои билеты

  • [x] Список регистраций пользователя
  • [x] Фильтры: предстоящие, прошедшие, отменённые
  • [x] Карточка билета: событие, дата, тип билета, статус, confirmation code
  • [x] QR-код для каждого билета
  • [x] Кнопка «Отменить регистрацию» (если разрешено)
  • [x] Кнопка «Повторно отправить билет» в Telegram

Детали билета

  • [x] Полная информация о событии
  • [x] QR-код (крупный, для сканирования)
  • [x] Confirmation code
  • [x] Добавить в календарь
  • [x] Поделиться

Профиль

  • [x] Просмотр и редактирование: firstName, lastName, email
  • [x] Смена пароля (если email-аккаунт)
  • [x] Привязка/отвязка Telegram
  • [x] Аватар (загрузка)

Настройки уведомлений

  • [x] Toggle для типов уведомлений
  • [x] Напоминания о событиях (on/off)
  • [x] Обновления регистраций (on/off)

Организации

  • [x] Список организаций, где пользователь член
  • [x] Для каждой: роль, кнопка перейти в dashboard
  • [x] Кнопка «Создать организацию» → запрос

Группы

  • [x] Список групп, в которых состоит
  • [x] Кнопка выхода из группы
  • [x] Присоединение по инвайт-коду

Definition of Done (DoD)

  • [x] Все Acceptance Criteria выполнены
  • [x] Responsive design
  • [x] Loading и error states
  • [x] Тесты для критических функций
  • [x] Code review пройден
  • [x] CI/CD pipeline проходит

Технические детали

Структура

frontend/app/(dashboard)/
├── dashboard/              — организаторский dashboard
└── account/                — личный кабинет участника
    ├── page.tsx            — мои билеты
    ├── profile/page.tsx    — профиль
    ├── notifications/page.tsx — настройки уведомлений
    └── organizations/page.tsx — организации

frontend/components/features/account/
├── my-tickets.tsx
├── ticket-card.tsx
├── ticket-detail.tsx
├── profile-form.tsx
├── notification-settings.tsx
└── organization-list.tsx

My Tickets

'use client';

import { useMyRegistrations } from '@/lib/hooks/use-registrations';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { TicketCard } from './ticket-card';

export function MyTickets() {
  const { data: registrations, isLoading } = useMyRegistrations();

  const upcoming = registrations?.filter(r =>
    r.status === 'CONFIRMED' && new Date(r.event.startsAt) > new Date()
  );

  const past = registrations?.filter(r =>
    r.status === 'CHECKED_IN' || new Date(r.event.startsAt) < new Date()
  );

  const cancelled = registrations?.filter(r =>
    r.status === 'CANCELLED'
  );

  return (
    <Tabs defaultValue="upcoming">
      <TabsList>
        <TabsTrigger value="upcoming">
          Предстоящие ({upcoming?.length || 0})
        </TabsTrigger>
        <TabsTrigger value="past">
          Прошедшие ({past?.length || 0})
        </TabsTrigger>
        <TabsTrigger value="cancelled">
          Отменённые ({cancelled?.length || 0})
        </TabsTrigger>
      </TabsList>

      <TabsContent value="upcoming">
        <div className="grid gap-4 md:grid-cols-2">
          {upcoming?.map(reg => (
            <TicketCard key={reg.id} registration={reg} />
          ))}
        </div>
      </TabsContent>
      {/* ... */}
    </Tabs>
  );
}

Ticket Card

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { QRCodeSVG } from 'qrcode.react';

export function TicketCard({ registration }) {
  return (
    <Card>
      <CardHeader className="flex flex-row items-start justify-between">
        <div>
          <CardTitle>{registration.event.title}</CardTitle>
          <p className="text-sm text-muted-foreground">
            {formatDate(registration.event.startsAt)}
          </p>
        </div>
        <Badge variant={getStatusVariant(registration.status)}>
          {getStatusLabel(registration.status)}
        </Badge>
      </CardHeader>
      <CardContent className="flex gap-4">
        <QRCodeSVG
          value={`https://aqstream.ru/check-in/${registration.confirmationCode}`}
          size={80}
        />
        <div className="flex-1">
          <p className="text-sm">
            <span className="text-muted-foreground">Тип билета:</span>{' '}
            {registration.ticketType.name}
          </p>
          <p className="text-sm font-mono">
            Код: {registration.confirmationCode}
          </p>
        </div>
      </CardContent>
      <CardFooter className="gap-2">
        <Button variant="outline" size="sm" asChild>
          <Link href={`/account/tickets/${registration.id}`}>
            Подробнее
          </Link>
        </Button>
        <Button variant="ghost" size="sm" onClick={resendTicket}>
          Отправить в Telegram
        </Button>
      </CardFooter>
    </Card>
  );
}

Для пользователей с организациями: - Dashboard — организаторский вид - Мои билеты — участника вид

Для пользователей без организаций: - Только личный кабинет

// Определить какой вид показывать
const { user } = useAuthStore();
const { data: memberships } = useOrganizationMemberships();

const hasOrganizations = memberships && memberships.length > 0;

// В sidebar показывать разные меню

Зависимости

Блокирует

  • Нет

Зависит от

Out of Scope

  • История платежей (Phase 3)
  • Избранные события
  • Подписка на организаторов
  • Social features

Заметки

  • Личный кабинет доступен всем авторизованным пользователям
  • Организаторы видят и dashboard и личный кабинет
  • QR-код генерируется на клиенте через qrcode.react
  • Рассмотреть добавление push notifications через Telegram