Compare commits
3 Commits
954dd473d1
...
db2eddca4b
| Author | SHA1 | Date | |
|---|---|---|---|
| db2eddca4b | |||
| cf131bb04e | |||
| f0a4ad177e |
120
CHANGELOG.md
Normal file
120
CHANGELOG.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 📝 История изменений MC Panel
|
||||
|
||||
## Версия 2.0 - Система тикетов (14.01.2026)
|
||||
|
||||
### ✨ Новые возможности
|
||||
|
||||
#### 🎫 Система тикетов
|
||||
- Полноценная система поддержки с чатом
|
||||
- Три статуса: На рассмотрении, В работе, Закрыт
|
||||
- Автоматическое обновление сообщений каждые 3 секунды
|
||||
- Системные сообщения о смене статуса
|
||||
- Кнопка "Тикеты" в header
|
||||
|
||||
#### 👥 Новая роль "Тех. поддержка"
|
||||
- Доступ ко всем тикетам
|
||||
- Возможность менять статусы тикетов
|
||||
- Возможность отвечать на тикеты
|
||||
- Отдельный бейдж в интерфейсе
|
||||
|
||||
#### 🔧 Улучшения управления пользователями
|
||||
- Выпадающий список для выбора роли
|
||||
- Три роли: Пользователь, Тех. поддержка, Администратор
|
||||
- Цветные индикаторы ролей
|
||||
- Описание прав для каждой роли
|
||||
|
||||
### 🔐 Безопасность
|
||||
- Изменён логин администратора: `Sofa12345`
|
||||
- Изменён пароль администратора: `arkonsad123`
|
||||
|
||||
### 📁 Новые файлы
|
||||
- `backend/tickets.json` - хранилище тикетов
|
||||
- `frontend/src/components/Tickets.jsx` - список тикетов
|
||||
- `frontend/src/components/TicketChat.jsx` - чат тикета
|
||||
- `frontend/src/components/CreateTicketModal.jsx` - создание тикета
|
||||
- `TICKETS_SYSTEM.md` - документация системы тикетов
|
||||
|
||||
---
|
||||
|
||||
## Версия 1.5 - Система тем (14.01.2026)
|
||||
|
||||
### 🎨 Темы
|
||||
- 5 тем: Тёмная, Светлая, Фиолетовая, Синяя, Зелёная
|
||||
- Градиентный логотип "MC Panel" для каждой темы
|
||||
- Селектор тем в header
|
||||
- Автоматическое сохранение выбранной темы
|
||||
|
||||
### 🎯 Дизайн
|
||||
- Современный интерфейс в стиле TimeWeb Cloud
|
||||
- Карточки с тенями и анимациями
|
||||
- Плавные переходы между темами
|
||||
- Адаптивный дизайн для мобильных
|
||||
|
||||
### 📁 Файлы
|
||||
- `frontend/src/themes.js` - конфигурация тем
|
||||
- `frontend/src/components/ThemeSelector.jsx` - селектор тем
|
||||
|
||||
---
|
||||
|
||||
## Версия 1.0 - Базовая панель (13.01.2026)
|
||||
|
||||
### 🖥️ Управление серверами
|
||||
- Создание и удаление серверов
|
||||
- Запуск и остановка серверов
|
||||
- Просмотр консоли в реальном времени
|
||||
- Менеджер файлов с редактированием
|
||||
- Мониторинг ресурсов (RAM, диск)
|
||||
- Настройки сервера
|
||||
|
||||
### 👥 Система пользователей
|
||||
- Регистрация и авторизация
|
||||
- JWT токены
|
||||
- Роли: Админ и Пользователь
|
||||
- Управление доступом к серверам
|
||||
- Владельцы серверов
|
||||
|
||||
### 🌐 Сетевой доступ
|
||||
- Работа через Radmin VPN
|
||||
- Автоматическое определение API URL
|
||||
- Поддержка локальной и сетевой работы
|
||||
|
||||
### 📁 Основные файлы
|
||||
- `backend/main.py` - FastAPI бэкенд
|
||||
- `frontend/src/App.jsx` - React фронтенд
|
||||
- `backend/users.json` - хранилище пользователей
|
||||
- `backend/servers/` - папка с серверами
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Планы на будущее
|
||||
|
||||
### Версия 2.1
|
||||
- [ ] Уведомления о новых сообщениях в тикетах
|
||||
- [ ] Прикрепление файлов к тикетам
|
||||
- [ ] Фильтрация тикетов по статусу
|
||||
- [ ] Поиск по тикетам
|
||||
|
||||
### Версия 2.2
|
||||
- [ ] Статистика по тикетам
|
||||
- [ ] Экспорт истории тикетов
|
||||
- [ ] Шаблоны ответов для тех. поддержки
|
||||
- [ ] Приоритеты тикетов
|
||||
|
||||
### Версия 3.0
|
||||
- [ ] Плагины для серверов
|
||||
- [ ] Автоматическое резервное копирование
|
||||
- [ ] Планировщик задач
|
||||
- [ ] Мониторинг производительности
|
||||
|
||||
---
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
Если у вас возникли вопросы или проблемы:
|
||||
1. Создайте тикет в системе поддержки
|
||||
2. Опишите проблему подробно
|
||||
3. Дождитесь ответа от тех. поддержки
|
||||
|
||||
**Учётные данные администратора:**
|
||||
- Логин: `Sofa12345`
|
||||
- Пароль: `arkonsad123`
|
||||
143
TICKETS_SYSTEM.md
Normal file
143
TICKETS_SYSTEM.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 🎫 Система тикетов
|
||||
|
||||
## Что добавлено
|
||||
|
||||
### ✅ Новые возможности
|
||||
|
||||
1. **Система тикетов** - полноценная система поддержки с чатом
|
||||
2. **Три статуса тикетов**:
|
||||
- 🟡 **На рассмотрении** (pending) - новый тикет
|
||||
- 🔵 **В работе** (in_progress) - тикет взят в работу
|
||||
- 🟢 **Закрыт** (closed) - тикет решён
|
||||
|
||||
3. **Новая роль "Тех. поддержка"** (support):
|
||||
- Доступ ко всем тикетам
|
||||
- Возможность менять статусы тикетов
|
||||
- Возможность отвечать на тикеты
|
||||
|
||||
4. **Кнопка "Тикеты"** в header рядом с кнопкой "Пользователи"
|
||||
|
||||
### 📋 Возможности по ролям
|
||||
|
||||
#### Обычные пользователи (user)
|
||||
- ✅ Создавать тикеты
|
||||
- ✅ Просматривать свои тикеты
|
||||
- ✅ Отправлять сообщения в свои тикеты
|
||||
- ❌ Менять статусы тикетов
|
||||
- ❌ Видеть чужие тикеты
|
||||
|
||||
#### Тех. поддержка (support)
|
||||
- ✅ Просматривать все тикеты
|
||||
- ✅ Отвечать на любые тикеты
|
||||
- ✅ Менять статусы тикетов
|
||||
- ✅ Закрывать тикеты
|
||||
- ❌ Управлять пользователями
|
||||
- ❌ Управлять серверами
|
||||
|
||||
#### Администраторы (admin)
|
||||
- ✅ Все возможности тех. поддержки
|
||||
- ✅ Управление пользователями
|
||||
- ✅ Управление серверами
|
||||
- ✅ Назначение ролей
|
||||
|
||||
## 🚀 Как использовать
|
||||
|
||||
### Создание тикета
|
||||
1. Нажмите кнопку "Тикеты" в header
|
||||
2. Нажмите "Создать тикет"
|
||||
3. Заполните тему и описание проблемы
|
||||
4. Нажмите "Создать"
|
||||
|
||||
### Работа с тикетом
|
||||
1. Откройте список тикетов
|
||||
2. Нажмите на нужный тикет
|
||||
3. Пишите сообщения в чат
|
||||
4. Тех. поддержка и админы могут менять статус тикета
|
||||
|
||||
### Назначение роли "Тех. поддержка"
|
||||
1. Войдите как администратор (none / none)
|
||||
2. Нажмите кнопку "Пользователи"
|
||||
3. Найдите нужного пользователя
|
||||
4. В выпадающем списке выберите "Тех. поддержка"
|
||||
5. Роль изменится автоматически
|
||||
|
||||
## 📁 Новые файлы
|
||||
|
||||
### Backend
|
||||
- `backend/tickets.json` - хранилище тикетов (создаётся автоматически)
|
||||
- Добавлены endpoints в `backend/main.py`:
|
||||
- `GET /api/tickets` - список тикетов
|
||||
- `POST /api/tickets/create` - создать тикет
|
||||
- `GET /api/tickets/{id}` - получить тикет
|
||||
- `POST /api/tickets/{id}/message` - добавить сообщение
|
||||
- `PUT /api/tickets/{id}/status` - изменить статус
|
||||
|
||||
### Frontend
|
||||
- `frontend/src/components/Tickets.jsx` - список тикетов
|
||||
- `frontend/src/components/TicketChat.jsx` - чат тикета
|
||||
- `frontend/src/components/CreateTicketModal.jsx` - создание тикета
|
||||
|
||||
## 🎨 Интерфейс
|
||||
|
||||
### Список тикетов
|
||||
- Карточки с информацией о тикете
|
||||
- Цветные индикаторы статуса
|
||||
- Количество сообщений
|
||||
- Дата создания
|
||||
- Автор тикета
|
||||
|
||||
### Чат тикета
|
||||
- Сообщения в реальном времени (обновление каждые 3 секунды)
|
||||
- Системные сообщения о смене статуса
|
||||
- Кнопки смены статуса (для тех. поддержки и админов)
|
||||
- Отправка сообщений (если тикет не закрыт)
|
||||
|
||||
## 🔧 Технические детали
|
||||
|
||||
### Статусы тикетов
|
||||
```javascript
|
||||
pending // На рассмотрении (жёлтый)
|
||||
in_progress // В работе (синий)
|
||||
closed // Закрыт (зелёный)
|
||||
```
|
||||
|
||||
### Роли пользователей
|
||||
```javascript
|
||||
user // Обычный пользователь
|
||||
support // Тех. поддержка
|
||||
admin // Администратор
|
||||
```
|
||||
|
||||
### Структура тикета
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"title": "Проблема с сервером",
|
||||
"description": "Описание проблемы",
|
||||
"author": "username",
|
||||
"status": "pending",
|
||||
"created_at": "2024-01-14T12:00:00",
|
||||
"updated_at": "2024-01-14T12:00:00",
|
||||
"messages": [
|
||||
{
|
||||
"author": "username",
|
||||
"text": "Текст сообщения",
|
||||
"timestamp": "2024-01-14T12:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ Готово!
|
||||
|
||||
Система тикетов полностью интегрирована в MC Panel. Пользователи могут создавать тикеты, а тех. поддержка и администраторы могут на них отвечать и управлять статусами.
|
||||
|
||||
### Учётные данные по умолчанию
|
||||
- **Логин**: Sofa12345
|
||||
- **Пароль**: arkonsad123
|
||||
- **Роль**: admin
|
||||
|
||||
Для создания пользователя тех. поддержки:
|
||||
1. Зарегистрируйте нового пользователя
|
||||
2. Войдите как админ
|
||||
3. Назначьте ему роль "Тех. поддержка"
|
||||
147
backend/main.py
147
backend/main.py
@@ -36,6 +36,7 @@ security = HTTPBearer(auto_error=False)
|
||||
SERVERS_DIR = Path("servers")
|
||||
SERVERS_DIR.mkdir(exist_ok=True)
|
||||
USERS_FILE = Path("users.json")
|
||||
TICKETS_FILE = Path("tickets.json")
|
||||
|
||||
server_processes: dict[str, subprocess.Popen] = {}
|
||||
server_logs: dict[str, list[str]] = {}
|
||||
@@ -46,13 +47,13 @@ IS_WINDOWS = sys.platform == 'win32'
|
||||
def init_users():
|
||||
if not USERS_FILE.exists():
|
||||
admin_user = {
|
||||
"username": "admin",
|
||||
"password": pwd_context.hash("admin"),
|
||||
"username": "Sofa12345",
|
||||
"password": pwd_context.hash("arkonsad123"),
|
||||
"role": "admin",
|
||||
"servers": []
|
||||
}
|
||||
save_users({"admin": admin_user})
|
||||
print("Создан пользователь по умолчанию: admin / admin")
|
||||
save_users({"Sofa12345": admin_user})
|
||||
print("Создан пользователь по умолчанию: none / none")
|
||||
|
||||
def load_users() -> dict:
|
||||
if USERS_FILE.exists():
|
||||
@@ -80,6 +81,17 @@ def save_server_config(server_name: str, config: dict):
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Функции для работы с тикетами
|
||||
def load_tickets() -> dict:
|
||||
if TICKETS_FILE.exists():
|
||||
with open(TICKETS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_tickets(tickets: dict):
|
||||
with open(TICKETS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(tickets, f, indent=2, ensure_ascii=False)
|
||||
|
||||
init_users()
|
||||
|
||||
# Функции аутентификации
|
||||
@@ -752,6 +764,133 @@ async def rename_file(server_name: str, old_path: str, new_name: str, user: dict
|
||||
old_file_path.rename(new_file_path)
|
||||
return {"message": "Файл переименован"}
|
||||
|
||||
# API для тикетов
|
||||
@app.get("/api/tickets")
|
||||
async def get_tickets(user: dict = Depends(get_current_user)):
|
||||
"""Получить список тикетов"""
|
||||
tickets = load_tickets()
|
||||
|
||||
# Админы и тех. поддержка видят все тикеты
|
||||
if user["role"] in ["admin", "support"]:
|
||||
return list(tickets.values())
|
||||
|
||||
# Обычные пользователи видят только свои тикеты
|
||||
user_tickets = [t for t in tickets.values() if t["author"] == user["username"]]
|
||||
return user_tickets
|
||||
|
||||
@app.post("/api/tickets/create")
|
||||
async def create_ticket(data: dict, user: dict = Depends(get_current_user)):
|
||||
"""Создать новый тикет"""
|
||||
tickets = load_tickets()
|
||||
|
||||
# Генерируем ID тикета
|
||||
ticket_id = str(len(tickets) + 1)
|
||||
|
||||
ticket = {
|
||||
"id": ticket_id,
|
||||
"title": data.get("title", "").strip(),
|
||||
"description": data.get("description", "").strip(),
|
||||
"author": user["username"],
|
||||
"status": "pending", # pending, in_progress, closed
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"updated_at": datetime.utcnow().isoformat(),
|
||||
"messages": [
|
||||
{
|
||||
"author": user["username"],
|
||||
"text": data.get("description", "").strip(),
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
tickets[ticket_id] = ticket
|
||||
save_tickets(tickets)
|
||||
|
||||
return {"message": "Тикет создан", "ticket": ticket}
|
||||
|
||||
@app.get("/api/tickets/{ticket_id}")
|
||||
async def get_ticket(ticket_id: str, user: dict = Depends(get_current_user)):
|
||||
"""Получить тикет по ID"""
|
||||
tickets = load_tickets()
|
||||
|
||||
if ticket_id not in tickets:
|
||||
raise HTTPException(404, "Тикет не найден")
|
||||
|
||||
ticket = tickets[ticket_id]
|
||||
|
||||
# Проверка доступа
|
||||
if user["role"] not in ["admin", "support"] and ticket["author"] != user["username"]:
|
||||
raise HTTPException(403, "Нет доступа к этому тикету")
|
||||
|
||||
return ticket
|
||||
|
||||
@app.post("/api/tickets/{ticket_id}/message")
|
||||
async def add_ticket_message(ticket_id: str, data: dict, user: dict = Depends(get_current_user)):
|
||||
"""Добавить сообщение в тикет"""
|
||||
tickets = load_tickets()
|
||||
|
||||
if ticket_id not in tickets:
|
||||
raise HTTPException(404, "Тикет не найден")
|
||||
|
||||
ticket = tickets[ticket_id]
|
||||
|
||||
# Проверка доступа
|
||||
if user["role"] not in ["admin", "support"] and ticket["author"] != user["username"]:
|
||||
raise HTTPException(403, "Нет доступа к этому тикету")
|
||||
|
||||
message = {
|
||||
"author": user["username"],
|
||||
"text": data.get("text", "").strip(),
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
ticket["messages"].append(message)
|
||||
ticket["updated_at"] = datetime.utcnow().isoformat()
|
||||
|
||||
tickets[ticket_id] = ticket
|
||||
save_tickets(tickets)
|
||||
|
||||
return {"message": "Сообщение добавлено", "ticket": ticket}
|
||||
|
||||
@app.put("/api/tickets/{ticket_id}/status")
|
||||
async def update_ticket_status(ticket_id: str, data: dict, user: dict = Depends(get_current_user)):
|
||||
"""Изменить статус тикета (только для админов и тех. поддержки)"""
|
||||
if user["role"] not in ["admin", "support"]:
|
||||
raise HTTPException(403, "Недостаточно прав")
|
||||
|
||||
tickets = load_tickets()
|
||||
|
||||
if ticket_id not in tickets:
|
||||
raise HTTPException(404, "Тикет не найден")
|
||||
|
||||
new_status = data.get("status")
|
||||
if new_status not in ["pending", "in_progress", "closed"]:
|
||||
raise HTTPException(400, "Неверный статус")
|
||||
|
||||
ticket = tickets[ticket_id]
|
||||
ticket["status"] = new_status
|
||||
ticket["updated_at"] = datetime.utcnow().isoformat()
|
||||
|
||||
# Добавляем системное сообщение о смене статуса
|
||||
status_names = {
|
||||
"pending": "На рассмотрении",
|
||||
"in_progress": "В работе",
|
||||
"closed": "Закрыт"
|
||||
}
|
||||
|
||||
message = {
|
||||
"author": "system",
|
||||
"text": f"Статус изменён на: {status_names[new_status]}",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
ticket["messages"].append(message)
|
||||
|
||||
tickets[ticket_id] = ticket
|
||||
save_tickets(tickets)
|
||||
|
||||
return {"message": "Статус обновлён", "ticket": ticket}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
48
backend/tickets.json
Normal file
48
backend/tickets.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"1": {
|
||||
"id": "1",
|
||||
"title": "Пошёл нахуй",
|
||||
"description": "Свин",
|
||||
"author": "arkonsad",
|
||||
"status": "closed",
|
||||
"created_at": "2026-01-14T15:20:26.344010",
|
||||
"updated_at": "2026-01-14T15:22:02.654579",
|
||||
"messages": [
|
||||
{
|
||||
"author": "arkonsad",
|
||||
"text": "Свин",
|
||||
"timestamp": "2026-01-14T15:20:26.344010"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "Ты че",
|
||||
"timestamp": "2026-01-14T15:21:19.943424"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "ахуел",
|
||||
"timestamp": "2026-01-14T15:21:24.251787"
|
||||
},
|
||||
{
|
||||
"author": "arkonsad",
|
||||
"text": "покушай говна",
|
||||
"timestamp": "2026-01-14T15:21:46.676746"
|
||||
},
|
||||
{
|
||||
"author": "system",
|
||||
"text": "Статус изменён на: В работе",
|
||||
"timestamp": "2026-01-14T15:21:48.504108"
|
||||
},
|
||||
{
|
||||
"author": "Sofa12345",
|
||||
"text": "тварина ты ебаная",
|
||||
"timestamp": "2026-01-14T15:21:58.245227"
|
||||
},
|
||||
{
|
||||
"author": "system",
|
||||
"text": "Статус изменён на: Закрыт",
|
||||
"timestamp": "2026-01-14T15:22:02.654579"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"password": "$2b$12$0AJU/Cc6vI.gqUY6BfU8E.6adiK3QS/1EyZJ98MAExiHAf4HOhn4C",
|
||||
"role": "admin",
|
||||
"servers": []
|
||||
},
|
||||
"MihailPrud": {
|
||||
"username": "MihailPrud",
|
||||
"password": "$2b$12$GfbQN4scE.b.mtUHofWWE.Dn1tQpT1zwLAxeICv90sHP4zGv0dc2G",
|
||||
@@ -13,5 +7,19 @@
|
||||
"test",
|
||||
"nya"
|
||||
]
|
||||
},
|
||||
"arkonsad": {
|
||||
"username": "arkonsad",
|
||||
"password": "$2b$12$z.AYkfa/MlTYFd9rLNfBmu9JHOFKUe8YdddnqCmRqAxc7vGQeo392",
|
||||
"role": "user",
|
||||
"servers": [
|
||||
"123"
|
||||
]
|
||||
},
|
||||
"Sofa12345": {
|
||||
"username": "Sofa12345",
|
||||
"password": "$2b$12$Fph20p2mwgOAqoT77wSA3.n1S7NiHLa28aiNOwWcz3PfNhgC5pp5.",
|
||||
"role": "admin",
|
||||
"servers": []
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X } from 'lucide-react';
|
||||
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X, MessageSquare } from 'lucide-react';
|
||||
import Console from './components/Console';
|
||||
import FileManager from './components/FileManager';
|
||||
import Stats from './components/Stats';
|
||||
import ServerSettings from './components/ServerSettings';
|
||||
import CreateServerModal from './components/CreateServerModal';
|
||||
import Users from './components/Users';
|
||||
import Tickets from './components/Tickets';
|
||||
import Auth from './components/Auth';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import ThemeSelector from './components/ThemeSelector';
|
||||
@@ -21,6 +22,7 @@ function App() {
|
||||
const [activeTab, setActiveTab] = useState('console');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showUsers, setShowUsers] = useState(false);
|
||||
const [showTickets, setShowTickets] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState(false);
|
||||
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
@@ -156,7 +158,7 @@ function App() {
|
||||
{user?.username}
|
||||
</span>
|
||||
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
|
||||
{user?.role === 'admin' ? 'Админ' : 'Пользователь'}
|
||||
{user?.role === 'admin' ? 'Админ' : user?.role === 'support' ? 'Поддержка' : 'Пользователь'}
|
||||
</span>
|
||||
</div>
|
||||
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||
@@ -183,6 +185,56 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
if (showTickets) {
|
||||
return (
|
||||
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
|
||||
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
|
||||
<Server className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>MC Panel</h1>
|
||||
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
|
||||
<span className={`text-sm ${currentTheme.textSecondary}`}>
|
||||
{user?.username}
|
||||
</span>
|
||||
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
|
||||
{user?.role === 'admin' ? 'Админ' : user?.role === 'support' ? 'Поддержка' : 'Пользователь'}
|
||||
</span>
|
||||
</div>
|
||||
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||
<button
|
||||
onClick={() => setShowTickets(false)}
|
||||
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
|
||||
>
|
||||
<Server className="w-4 h-4" />
|
||||
Серверы
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Выход
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<Tickets token={token} user={user} theme={currentTheme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
|
||||
{/* Header */}
|
||||
@@ -217,10 +269,17 @@ function App() {
|
||||
{user?.username}
|
||||
</span>
|
||||
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
|
||||
{user?.role === 'admin' ? 'Админ' : 'Пользователь'}
|
||||
{user?.role === 'admin' ? 'Админ' : user?.role === 'support' ? 'Поддержка' : 'Пользователь'}
|
||||
</span>
|
||||
</div>
|
||||
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||
<button
|
||||
onClick={() => setShowTickets(true)}
|
||||
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Тикеты</span>
|
||||
</button>
|
||||
{user?.role === 'admin' && (
|
||||
<button
|
||||
onClick={() => setShowUsers(true)}
|
||||
|
||||
@@ -133,14 +133,14 @@ export default function Auth({ onLogin }) {
|
||||
{isLogin && (
|
||||
<div className={`mt-6 text-center text-sm ${currentTheme.textSecondary}`}>
|
||||
<p>Учётные данные по умолчанию:</p>
|
||||
<p className={`${currentTheme.text} font-mono mt-1`}>admin / admin</p>
|
||||
<p className={`${currentTheme.text} font-mono mt-1`}>none / none</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className={`text-center mt-6 text-sm ${currentTheme.textSecondary}`}>
|
||||
<p>© 2024 MC Panel. Все права защищены.</p>
|
||||
<p>© 2026 MC Panel. Все права защищены.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
93
frontend/src/components/CreateTicketModal.jsx
Normal file
93
frontend/src/components/CreateTicketModal.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '../config';
|
||||
|
||||
export default function CreateTicketModal({ token, theme, onClose, onCreated }) {
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/api/tickets/create`,
|
||||
formData,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
onCreated();
|
||||
} catch (error) {
|
||||
alert(error.response?.data?.detail || 'Ошибка создания тикета');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className={`${theme.secondary} rounded-2xl p-6 w-full max-w-md shadow-2xl ${theme.border} border`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className={`text-xl font-bold ${theme.text}`}>Создать тикет</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`${theme.textSecondary} hover:${theme.text} transition`}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||
Тема тикета
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||
placeholder="Краткое описание проблемы"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||
Описание
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition resize-none`}
|
||||
placeholder="Подробное описание проблемы"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={`flex-1 ${theme.card} ${theme.hover} px-4 py-2 rounded-xl transition`}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`flex-1 ${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl disabled:opacity-50 transition text-white`}
|
||||
>
|
||||
{loading ? 'Создание...' : 'Создать'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
241
frontend/src/components/TicketChat.jsx
Normal file
241
frontend/src/components/TicketChat.jsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { ArrowLeft, Send, Clock, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '../config';
|
||||
|
||||
export default function TicketChat({ ticket, token, user, theme, onBack }) {
|
||||
const [messages, setMessages] = useState(ticket.messages || []);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [currentTicket, setCurrentTicket] = useState(ticket);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
const interval = setInterval(loadTicket, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const loadTicket = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(`${API_URL}/api/tickets/${ticket.id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
setCurrentTicket(data);
|
||||
setMessages(data.messages || []);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки тикета:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!newMessage.trim() || loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
`${API_URL}/api/tickets/${ticket.id}/message`,
|
||||
{ text: newMessage.trim() },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
setMessages(data.ticket.messages);
|
||||
setCurrentTicket(data.ticket);
|
||||
setNewMessage('');
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки сообщения:', error);
|
||||
alert(error.response?.data?.detail || 'Ошибка отправки сообщения');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changeStatus = async (newStatus) => {
|
||||
try {
|
||||
const { data } = await axios.put(
|
||||
`${API_URL}/api/tickets/${ticket.id}/status`,
|
||||
{ status: newStatus },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
setCurrentTicket(data.ticket);
|
||||
setMessages(data.ticket.messages);
|
||||
} catch (error) {
|
||||
console.error('Ошибка изменения статуса:', error);
|
||||
alert(error.response?.data?.detail || 'Ошибка изменения статуса');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Clock className="w-5 h-5 text-yellow-500" />;
|
||||
case 'in_progress':
|
||||
return <AlertCircle className="w-5 h-5 text-blue-500" />;
|
||||
case 'closed':
|
||||
return <CheckCircle className="w-5 h-5 text-green-500" />;
|
||||
default:
|
||||
return <Clock className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'На рассмотрении';
|
||||
case 'in_progress':
|
||||
return 'В работе';
|
||||
case 'closed':
|
||||
return 'Закрыт';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50';
|
||||
case 'in_progress':
|
||||
return 'bg-blue-500/20 text-blue-500 border-blue-500/50';
|
||||
case 'closed':
|
||||
return 'bg-green-500/20 text-green-500 border-green-500/50';
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const canChangeStatus = user.role === 'admin' || user.role === 'support';
|
||||
|
||||
return (
|
||||
<div className={`h-full flex flex-col ${theme.primary}`}>
|
||||
{/* Header */}
|
||||
<div className={`${theme.secondary} ${theme.border} border-b p-4`}>
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className={`${theme.hover} p-2 rounded-lg transition`}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-bold">{currentTicket.title}</h2>
|
||||
<p className={`text-sm ${theme.textSecondary}`}>
|
||||
Автор: {currentTicket.author} • Создан: {new Date(currentTicket.created_at).toLocaleString('ru-RU')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`px-3 py-2 rounded-lg border flex items-center gap-2 ${getStatusColor(currentTicket.status)}`}>
|
||||
{getStatusIcon(currentTicket.status)}
|
||||
<span className="font-medium">{getStatusText(currentTicket.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Controls */}
|
||||
{canChangeStatus && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => changeStatus('pending')}
|
||||
disabled={currentTicket.status === 'pending'}
|
||||
className={`flex-1 px-3 py-2 rounded-lg border transition ${
|
||||
currentTicket.status === 'pending'
|
||||
? 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50'
|
||||
: `${theme.card} ${theme.hover} ${theme.border}`
|
||||
}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 inline mr-2" />
|
||||
На рассмотрении
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeStatus('in_progress')}
|
||||
disabled={currentTicket.status === 'in_progress'}
|
||||
className={`flex-1 px-3 py-2 rounded-lg border transition ${
|
||||
currentTicket.status === 'in_progress'
|
||||
? 'bg-blue-500/20 text-blue-500 border-blue-500/50'
|
||||
: `${theme.card} ${theme.hover} ${theme.border}`
|
||||
}`}
|
||||
>
|
||||
<AlertCircle className="w-4 h-4 inline mr-2" />
|
||||
В работе
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeStatus('closed')}
|
||||
disabled={currentTicket.status === 'closed'}
|
||||
className={`flex-1 px-3 py-2 rounded-lg border transition ${
|
||||
currentTicket.status === 'closed'
|
||||
? 'bg-green-500/20 text-green-500 border-green-500/50'
|
||||
: `${theme.card} ${theme.hover} ${theme.border}`
|
||||
}`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 inline mr-2" />
|
||||
Закрыт
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${msg.author === user.username ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[70%] rounded-2xl px-4 py-3 ${
|
||||
msg.author === 'system'
|
||||
? `${theme.tertiary} ${theme.border} border text-center`
|
||||
: msg.author === user.username
|
||||
? `${theme.accent} text-white`
|
||||
: `${theme.card} ${theme.border} border`
|
||||
}`}
|
||||
>
|
||||
{msg.author !== 'system' && msg.author !== user.username && (
|
||||
<div className={`text-xs font-semibold mb-1 ${theme.textSecondary}`}>
|
||||
{msg.author}
|
||||
</div>
|
||||
)}
|
||||
<div className="whitespace-pre-wrap break-words">{msg.text}</div>
|
||||
<div className={`text-xs mt-1 ${
|
||||
msg.author === user.username ? 'text-white/70' : theme.textSecondary
|
||||
}`}>
|
||||
{new Date(msg.timestamp).toLocaleTimeString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
{currentTicket.status !== 'closed' && (
|
||||
<form onSubmit={sendMessage} className={`${theme.border} border-t p-4`}>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Введите сообщение..."
|
||||
disabled={loading}
|
||||
className={`flex-1 ${theme.input} ${theme.border} border rounded-xl px-4 py-3 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !newMessage.trim()}
|
||||
className={`${theme.accent} ${theme.accentHover} px-6 py-3 rounded-xl flex items-center gap-2 text-white transition disabled:opacity-50`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
Отправить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
179
frontend/src/components/Tickets.jsx
Normal file
179
frontend/src/components/Tickets.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { MessageSquare, Plus, Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '../config';
|
||||
import TicketChat from './TicketChat';
|
||||
import CreateTicketModal from './CreateTicketModal';
|
||||
|
||||
export default function Tickets({ token, user, theme }) {
|
||||
const [tickets, setTickets] = useState([]);
|
||||
const [selectedTicket, setSelectedTicket] = useState(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadTickets();
|
||||
const interval = setInterval(loadTickets, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const loadTickets = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(`${API_URL}/api/tickets`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
setTickets(data);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки тикетов:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTicketCreated = () => {
|
||||
setShowCreateModal(false);
|
||||
loadTickets();
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Clock className="w-4 h-4 text-yellow-500" />;
|
||||
case 'in_progress':
|
||||
return <AlertCircle className="w-4 h-4 text-blue-500" />;
|
||||
case 'closed':
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||
default:
|
||||
return <Clock className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'На рассмотрении';
|
||||
case 'in_progress':
|
||||
return 'В работе';
|
||||
case 'closed':
|
||||
return 'Закрыт';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50';
|
||||
case 'in_progress':
|
||||
return 'bg-blue-500/20 text-blue-500 border-blue-500/50';
|
||||
case 'closed':
|
||||
return 'bg-green-500/20 text-green-500 border-green-500/50';
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedTicket) {
|
||||
return (
|
||||
<TicketChat
|
||||
ticket={selectedTicket}
|
||||
token={token}
|
||||
user={user}
|
||||
theme={theme}
|
||||
onBack={() => {
|
||||
setSelectedTicket(null);
|
||||
loadTickets();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`h-full ${theme.primary} ${theme.text} p-6`}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">Тикеты</h1>
|
||||
<p className={theme.textSecondary}>Система поддержки</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className={`${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl flex items-center gap-2 text-white transition`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Создать тикет
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tickets List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className={theme.textSecondary}>Загрузка тикетов...</p>
|
||||
</div>
|
||||
) : tickets.length === 0 ? (
|
||||
<div className={`${theme.card} ${theme.border} border rounded-2xl p-12 text-center`}>
|
||||
<MessageSquare className={`w-16 h-16 mx-auto mb-4 ${theme.textSecondary} opacity-50`} />
|
||||
<p className="text-lg font-medium mb-2">Нет тикетов</p>
|
||||
<p className={`text-sm ${theme.textSecondary} mb-4`}>
|
||||
Создайте первый тикет для обращения в поддержку
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className={`${theme.accent} ${theme.accentHover} px-6 py-2 rounded-xl text-white transition`}
|
||||
>
|
||||
Создать тикет
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{tickets.map((ticket) => (
|
||||
<div
|
||||
key={ticket.id}
|
||||
onClick={() => setSelectedTicket(ticket)}
|
||||
className={`${theme.card} ${theme.border} border rounded-2xl p-6 cursor-pointer ${theme.hover} transition-all duration-200`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold mb-2">{ticket.title}</h3>
|
||||
<p className={`text-sm ${theme.textSecondary} line-clamp-2`}>
|
||||
{ticket.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-lg border flex items-center gap-2 ${getStatusColor(ticket.status)}`}>
|
||||
{getStatusIcon(ticket.status)}
|
||||
<span className="text-sm font-medium">{getStatusText(ticket.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className={theme.textSecondary}>
|
||||
Автор: <span className={theme.text}>{ticket.author}</span>
|
||||
</span>
|
||||
<span className={theme.textSecondary}>•</span>
|
||||
<span className={theme.textSecondary}>
|
||||
Сообщений: <span className={theme.text}>{ticket.messages?.length || 0}</span>
|
||||
</span>
|
||||
<span className={theme.textSecondary}>•</span>
|
||||
<span className={theme.textSecondary}>
|
||||
{new Date(ticket.created_at).toLocaleString('ru-RU')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCreateModal && (
|
||||
<CreateTicketModal
|
||||
token={token}
|
||||
theme={theme}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onCreated={handleTicketCreated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,13 +50,7 @@ export default function Users({ token }) {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRole = async (username, currentRole) => {
|
||||
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
||||
|
||||
if (!confirm(`Изменить роль пользователя ${username} на ${newRole}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changeRole = async (username, newRole) => {
|
||||
try {
|
||||
await axios.put(
|
||||
`${API_URL}/api/users/${username}/role`,
|
||||
@@ -108,7 +102,7 @@ export default function Users({ token }) {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded ${
|
||||
user.role === 'admin' ? 'bg-blue-600' : 'bg-gray-700'
|
||||
user.role === 'admin' ? 'bg-blue-600' : user.role === 'support' ? 'bg-purple-600' : 'bg-gray-700'
|
||||
}`}>
|
||||
{user.role === 'admin' ? (
|
||||
<Shield className="w-6 h-6" />
|
||||
@@ -119,18 +113,21 @@ export default function Users({ token }) {
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{user.username}</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
{user.role === 'admin' ? 'Администратор' : 'Пользователь'}
|
||||
{user.role === 'admin' ? 'Администратор' : user.role === 'support' ? 'Тех. поддержка' : 'Пользователь'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => toggleRole(user.username, user.role)}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm"
|
||||
<select
|
||||
value={user.role}
|
||||
onChange={(e) => changeRole(user.username, e.target.value)}
|
||||
className="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded text-sm border border-gray-600 focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{user.role === 'admin' ? 'Сделать пользователем' : 'Сделать админом'}
|
||||
</button>
|
||||
<option value="user">Пользователь</option>
|
||||
<option value="support">Тех. поддержка</option>
|
||||
<option value="admin">Администратор</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => deleteUser(user.username)}
|
||||
className="bg-red-600 hover:bg-red-700 p-2 rounded"
|
||||
@@ -141,7 +138,7 @@ export default function Users({ token }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.role !== 'admin' && (
|
||||
{user.role === 'user' && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2 text-gray-400">
|
||||
Доступ к серверам:
|
||||
@@ -175,6 +172,12 @@ export default function Users({ token }) {
|
||||
Администратор имеет доступ ко всем серверам
|
||||
</p>
|
||||
)}
|
||||
|
||||
{user.role === 'support' && (
|
||||
<p className="text-sm text-gray-400">
|
||||
Тех. поддержка имеет доступ к системе тикетов
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,8 @@ http://localhost:3000
|
||||
```
|
||||
|
||||
### 4️⃣ Войдите в систему
|
||||
- **Логин**: `admin`
|
||||
- **Пароль**: `admin`
|
||||
- **Логин**: `Sofa12345`
|
||||
- **Пароль**: `arkonsad123`
|
||||
|
||||
## 🎨 Смена темы
|
||||
|
||||
@@ -57,6 +57,16 @@ http://localhost:3000
|
||||
1. Нажмите кнопку "Пользователи" в header
|
||||
2. Создайте новых пользователей
|
||||
3. Выдайте доступ к серверам
|
||||
4. Назначьте роли (Пользователь, Тех. поддержка, Администратор)
|
||||
|
||||
### Система тикетов 🎫
|
||||
1. Нажмите кнопку "Тикеты" в header
|
||||
2. Создайте тикет с описанием проблемы
|
||||
3. Общайтесь в чате тикета
|
||||
4. Тех. поддержка и админы могут менять статусы:
|
||||
- 🟡 На рассмотрении
|
||||
- 🔵 В работе
|
||||
- 🟢 Закрыт
|
||||
|
||||
### Выдача доступа к серверу
|
||||
1. Выберите сервер
|
||||
|
||||
166
ГОТОВО.md
Normal file
166
ГОТОВО.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# ✅ MC Panel готова к использованию!
|
||||
|
||||
## 🎉 Что сделано
|
||||
|
||||
### 1. Изменён логин администратора
|
||||
- **Старый**: admin / admin
|
||||
- **Новый**: Sofa12345 / arkonsad123
|
||||
|
||||
### 2. Добавлена система тикетов 🎫
|
||||
- Кнопка "Тикеты" в header рядом с "Пользователи"
|
||||
- Создание тикетов с темой и описанием
|
||||
- Чат для общения в тикете
|
||||
- Три статуса:
|
||||
- 🟡 На рассмотрении
|
||||
- 🔵 В работе
|
||||
- 🟢 Закрыт
|
||||
|
||||
### 3. Добавлена роль "Тех. поддержка" 👨💻
|
||||
- Доступ ко всем тикетам
|
||||
- Возможность менять статусы
|
||||
- Возможность отвечать на тикеты
|
||||
- Отдельный бейдж в интерфейсе
|
||||
|
||||
### 4. Улучшено управление пользователями
|
||||
- Выпадающий список для выбора роли
|
||||
- Три роли: Пользователь, Тех. поддержка, Администратор
|
||||
- Цветные индикаторы ролей
|
||||
|
||||
## 🚀 Запуск панели
|
||||
|
||||
### Шаг 1: Запустите бэкенд
|
||||
```bash
|
||||
cd backend
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Шаг 2: Запустите фронтенд
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Шаг 3: Откройте в браузере
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
### Шаг 4: Войдите как администратор
|
||||
- **Логин**: none
|
||||
- **Пароль**: none
|
||||
|
||||
## 📋 Быстрый старт
|
||||
|
||||
### Создание пользователя тех. поддержки
|
||||
1. Зарегистрируйте нового пользователя
|
||||
2. Войдите как админ (Sofa12345)
|
||||
3. Нажмите "Пользователи"
|
||||
4. Найдите нового пользователя
|
||||
5. В выпадающем списке выберите "Тех. поддержка"
|
||||
|
||||
### Создание тикета
|
||||
1. Войдите как обычный пользователь
|
||||
2. Нажмите кнопку "Тикеты" в header
|
||||
3. Нажмите "Создать тикет"
|
||||
4. Заполните тему и описание
|
||||
5. Нажмите "Создать"
|
||||
|
||||
### Работа с тикетом (тех. поддержка)
|
||||
1. Войдите как пользователь с ролью "Тех. поддержка"
|
||||
2. Нажмите "Тикеты"
|
||||
3. Выберите тикет из списка
|
||||
4. Отвечайте на сообщения
|
||||
5. Меняйте статус тикета кнопками вверху
|
||||
|
||||
## 🎨 Возможности
|
||||
|
||||
### Для всех пользователей
|
||||
- ✅ Создание серверов
|
||||
- ✅ Управление своими серверами
|
||||
- ✅ Создание тикетов
|
||||
- ✅ Общение в своих тикетах
|
||||
- ✅ Смена темы интерфейса
|
||||
|
||||
### Для тех. поддержки
|
||||
- ✅ Все возможности пользователя
|
||||
- ✅ Просмотр всех тикетов
|
||||
- ✅ Ответы на любые тикеты
|
||||
- ✅ Изменение статусов тикетов
|
||||
|
||||
### Для администраторов
|
||||
- ✅ Все возможности тех. поддержки
|
||||
- ✅ Управление пользователями
|
||||
- ✅ Назначение ролей
|
||||
- ✅ Удаление пользователей
|
||||
- ✅ Управление доступом к серверам
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
MC Panel/
|
||||
├── backend/
|
||||
│ ├── main.py # Основной файл бэкенда
|
||||
│ ├── users.json # База пользователей
|
||||
│ ├── tickets.json # База тикетов (создаётся автоматически)
|
||||
│ └── servers/ # Папка с серверами
|
||||
│
|
||||
├── frontend/
|
||||
│ └── src/
|
||||
│ ├── App.jsx # Главный компонент
|
||||
│ ├── themes.js # Конфигурация тем
|
||||
│ └── components/
|
||||
│ ├── Auth.jsx # Авторизация
|
||||
│ ├── Tickets.jsx # Список тикетов
|
||||
│ ├── TicketChat.jsx # Чат тикета
|
||||
│ ├── CreateTicketModal.jsx # Создание тикета
|
||||
│ ├── Users.jsx # Управление пользователями
|
||||
│ ├── Console.jsx # Консоль сервера
|
||||
│ ├── FileManager.jsx # Менеджер файлов
|
||||
│ ├── Stats.jsx # Статистика
|
||||
│ └── ServerSettings.jsx # Настройки сервера
|
||||
│
|
||||
└── Документация/
|
||||
├── ГОТОВО.md # Этот файл
|
||||
├── TICKETS_SYSTEM.md # Документация системы тикетов
|
||||
├── CHANGELOG.md # История изменений
|
||||
└── БЫСТРЫЙ_СТАРТ.md # Быстрый старт
|
||||
```
|
||||
|
||||
## 🎯 Что дальше?
|
||||
|
||||
### Тестирование
|
||||
1. Создайте несколько пользователей
|
||||
2. Назначьте одному роль "Тех. поддержка"
|
||||
3. Создайте тикет от имени обычного пользователя
|
||||
4. Ответьте на тикет от имени тех. поддержки
|
||||
5. Измените статус тикета
|
||||
|
||||
### Настройка
|
||||
1. Измените темы под свой вкус в `frontend/src/themes.js`
|
||||
2. Настройте порты в конфигурации
|
||||
3. Добавьте свои серверы
|
||||
|
||||
### Развёртывание
|
||||
1. Настройте production сборку фронтенда
|
||||
2. Настройте HTTPS для безопасности
|
||||
3. Настройте базу данных вместо JSON файлов
|
||||
4. Настройте резервное копирование
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
Если возникли вопросы:
|
||||
1. Прочитайте `TICKETS_SYSTEM.md`
|
||||
2. Прочитайте `CHANGELOG.md`
|
||||
3. Создайте тикет в системе
|
||||
|
||||
## ✨ Готово!
|
||||
|
||||
Панель MC Panel полностью готова к использованию со всеми функциями:
|
||||
- ✅ Управление серверами
|
||||
- ✅ Система пользователей
|
||||
- ✅ Система тикетов
|
||||
- ✅ Роль тех. поддержки
|
||||
- ✅ 5 тем оформления
|
||||
- ✅ Современный интерфейс
|
||||
|
||||
**Наслаждайтесь использованием! 🚀**
|
||||
Reference in New Issue
Block a user