Панель расхода с фильтрами

Сделай панель расхода по проектам. Сверху чипы-фильтры по статусу, которые можно снимать. Под ними на каждый проект строка с названием, круговым индикатором заполненности квоты в процентах и меткой статуса. Список постраничный, по пять строк. Если после фильтрации ничего не осталось, вместо списка показывается пустое состояние с кнопкой сброса фильтров.

usage-dashboard__docs__claude-code__claude-opus-5__1 · 23.09.2026, 20:20:27 мск

Проверки пройдены
Доки · claude-opus-5 · Claude Code
Контекст до задачи~22 702
Токенов на прогон50 733
Цена$0.332
Ошибок компилятора0

Первый вызов модели получил 47 387 токенов входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.

Компилятор

Ошибок нет: все компоненты и пропсы существуют.

Линтер и покрытие

  • inline-styleСтроки 72, 73, 103, 117, 127, 128
    Инлайновые стили вместо классов и токенов библиотеки
Компоненты библиотеки

JxBadge, JxButton, JxChip, JxEmptyState, JxPagination, JxProgressCircle

Ожидались JxChip, JxProgressCircle, JxBadge, JxPagination, JxEmptyState · покрытие 100%

Контекст

Доки (llms-full.txt, ~22548)

Вход 2 · выход 3 346 · из кэша 8 233 · в кэш 39 152 · 38 с

Рендер

Код компонента

tsx145 строк
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { useMemo, useState } from 'react';
import {
  JxBadge,
  JxButton,
  JxChip,
  JxEmptyState,
  JxPagination,
  JxProgressCircle,
} from '@jinx-ui/react';

type ProjectStatus = 'active' | 'paused' | 'over';

interface Project {
  id: string;
  name: string;
  used: number;
  quota: number;
  status: ProjectStatus;
}

const STATUS_META: Record<ProjectStatus, { label: string; tone: 'success' | 'warning' | 'danger' }> = {
  active: { label: 'Активен', tone: 'success' },
  paused: { label: 'На паузе', tone: 'warning' },
  over: { label: 'Превышен', tone: 'danger' },
};

const ALL_STATUSES: ProjectStatus[] = ['active', 'paused', 'over'];

const PROJECTS: Project[] = [
  { id: 'p1', name: 'Витрина каталога', used: 420, quota: 1000, status: 'active' },
  { id: 'p2', name: 'Платёжный шлюз', used: 980, quota: 1000, status: 'over' },
  { id: 'p3', name: 'Мобильный клиент', used: 310, quota: 800, status: 'active' },
  { id: 'p4', name: 'Аналитика событий', used: 640, quota: 900, status: 'paused' },
  { id: 'p5', name: 'Поиск по документам', used: 120, quota: 500, status: 'active' },
  { id: 'p6', name: 'Импорт из CRM', used: 700, quota: 700, status: 'over' },
  { id: 'p7', name: 'Уведомления', used: 210, quota: 600, status: 'paused' },
  { id: 'p8', name: 'Личный кабинет', used: 455, quota: 1000, status: 'active' },
  { id: 'p9', name: 'Отчёты для бухгалтерии', used: 890, quota: 1200, status: 'active' },
  { id: 'p10', name: 'Онбординг', used: 150, quota: 400, status: 'paused' },
  { id: 'p11', name: 'Партнёрский API', used: 1100, quota: 1000, status: 'over' },
  { id: 'p12', name: 'Внутренние инструменты', used: 260, quota: 900, status: 'active' },
];

const PAGE_SIZE = 5;

export default function ProjectQuotaPanel() {
  const [filters, setFilters] = useState<ProjectStatus[]>(ALL_STATUSES);
  const [page, setPage] = useState(1);

  const visible = useMemo(
    () => PROJECTS.filter((project) => filters.includes(project.status)),
    [filters],
  );

  const pageCount = Math.max(1, Math.ceil(visible.length / PAGE_SIZE));
  const currentPage = Math.min(page, pageCount);
  const rows = visible.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);

  const toggleStatus = (status: ProjectStatus) => {
    setFilters((prev) =>
      prev.includes(status) ? prev.filter((item) => item !== status) : [...prev, status],
    );
    setPage(1);
  };

  const resetFilters = () => {
    setFilters(ALL_STATUSES);
    setPage(1);
  };

  return (
    <section style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
        {ALL_STATUSES.map((status) => {
          const selected = filters.includes(status);
          return (
            <JxChip
              key={status}
              active={selected}
              removable={selected}
              onClick={() => toggleStatus(status)}
              onRemove={() => toggleStatus(status)}
            >
              {STATUS_META[status].label}
            </JxChip>
          );
        })}
      </div>

      {rows.length === 0 ? (
        <JxEmptyState
          title="Проектов не найдено"
          message="Ни один проект не подходит под выбранные фильтры статуса."
          action={
            <JxButton variant="primary" onClick={resetFilters}>
              Сбросить фильтры
            </JxButton>
          }
        />
      ) : (
        <>
          <ul
            style={{
              listStyle: 'none',
              margin: 0,
              padding: 0,
              display: 'flex',
              flexDirection: 'column',
              gap: '8px',
            }}
          >
            {rows.map((project) => {
              const meta = STATUS_META[project.status];
              return (
                <li
                  key={project.id}
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    gap: '16px',
                    padding: '12px 16px',
                    background: 'var(--jx-surface)',
                    border: '1px solid var(--jx-border)',
                    borderRadius: 'var(--jx-r)',
                  }}
                >
                  <span style={{ flex: 1, minWidth: 0, color: 'var(--jx-text)' }}>{project.name}</span>
                  <span style={{ color: 'var(--jx-text-3)', fontFamily: 'var(--jx-font-mono)' }}>
                    {project.used} / {project.quota}
                  </span>
                  <JxProgressCircle value={project.used} max={project.quota} showValue />
                  <JxBadge tone={meta.tone} dot>
                    {meta.label}
                  </JxBadge>
                </li>
              );
            })}
          </ul>

          <JxPagination total={pageCount} page={currentPage} onPageChange={setPage} />
        </>
      )}
    </section>
  );
}