a34c9f07a7
This reverts commit 4b0fc594b4.
295 lines
14 KiB
HTML
295 lines
14 KiB
HTML
{% extends "admin/base.html" %}
|
||
{% block title %}{{ target_user.username }} — Админ-панель{% endblock %}
|
||
{% block nav_users %}active{% endblock %}
|
||
{% block page_title %}
|
||
👤 {{ target_user.username }}
|
||
{% if target_user.is_admin %}<span class="a-badge a-badge-admin" style="margin-left:0.5rem">Админ</span>{% endif %}
|
||
{% if target_user.is_banned %}<span class="a-badge a-badge-banned" style="margin-left:0.5rem">Забанен</span>{% endif %}
|
||
{% endblock %}
|
||
{% block top_actions %}
|
||
<a href="/admin/users" class="at-back">← Назад</a>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="a-stats" style="grid-template-columns:repeat(4,1fr)">
|
||
<div class="a-stat">
|
||
<div class="as-label">Баланс</div>
|
||
<div class="as-value">{{ "%.0f"|format(target_user.balance) }} ₽</div>
|
||
</div>
|
||
<div class="a-stat">
|
||
<div class="as-label">Предметов</div>
|
||
<div class="as-value">{{ inventory|length }}</div>
|
||
</div>
|
||
<div class="a-stat">
|
||
<div class="as-label">Открытий кейсов</div>
|
||
<div class="as-value">{{ total_openings }}</div>
|
||
</div>
|
||
<div class="a-stat">
|
||
<div class="as-label">Контрактов</div>
|
||
<div class="as-value">{{ total_contracts }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RPU -->
|
||
<div class="a-card">
|
||
<div class="a-card-header">🎲 РПУ (Режим Персонального Угнетения)</div>
|
||
<div class="a-row a-gap-sm a-mb" id="rpuStats" style="justify-content:space-around;text-align:center">
|
||
<div><div class="as-value" style="font-size:1.4rem" id="rpuLevel">--</div><div class="as-label" style="font-size:0.7rem">Уровень</div></div>
|
||
<div><div class="as-value" style="font-size:1.4rem" id="rpuLuck">--</div><div class="as-label" style="font-size:0.7rem">Множитель</div></div>
|
||
<div><div class="as-value" style="font-size:1.4rem" id="rpuSpent">--</div><div class="as-label" style="font-size:0.7rem">Потрачено</div></div>
|
||
<div><div class="as-value" style="font-size:1.4rem" id="rpuOpened">--</div><div class="as-label" style="font-size:0.7rem">Открыто</div></div>
|
||
<div><div class="as-value" style="font-size:1.4rem" id="rpuAutoStatus">--</div><div class="as-label" style="font-size:0.7rem">Авто</div></div>
|
||
</div>
|
||
<div class="a-flex" style="flex-wrap:wrap">
|
||
<button class="a-btn a-btn-primary" onclick="openRPUModal()">⚙️ Настроить</button>
|
||
<button class="a-btn" style="border-color:#10b981;color:#10b981" onclick="setRPUPreset('lucky')">🍀 Везучий</button>
|
||
<button class="a-btn" style="border-color:#f59e0b;color:#f59e0b" onclick="setRPUPreset('normal')">📊 Обычный</button>
|
||
<button class="a-btn" style="border-color:#ef4444;color:#ef4444" onclick="setRPUPreset('unlucky')">🌧️ Невезучий</button>
|
||
<button class="a-btn a-btn-danger" onclick="setRPUPreset('very_unlucky')">💀 Проклятый</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Действия -->
|
||
<div class="a-card">
|
||
<div class="a-card-header">🔧 Действия</div>
|
||
<div class="a-flex" style="flex-wrap:wrap">
|
||
<button class="a-btn a-btn-primary" onclick="openBalanceModal()">💰 Баланс</button>
|
||
<button class="a-btn" onclick="openGiveItemModal()">🎁 Выдать предмет</button>
|
||
{% if target_user.id != user.id %}
|
||
<button class="a-btn" onclick="toggleAdmin()">{{ "👑 Снять админа" if target_user.is_admin else "👑 Сделать админом" }}</button>
|
||
{% endif %}
|
||
<button class="a-btn {{ 'a-btn-danger' if not target_user.is_banned else 'a-btn-success' }}" onclick="toggleBan()">
|
||
{{ "🔨 Забанить" if not target_user.is_banned else "✅ Разбанить" }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Инвентарь -->
|
||
<div class="a-card">
|
||
<div class="a-card-header">🎒 Инвентарь (последние 50)</div>
|
||
<div class="a-table-wrap">
|
||
<table class="a-table">
|
||
<thead><tr><th>ID</th><th>Название</th><th>Редкость</th><th>Float</th><th></th></tr></thead>
|
||
<tbody>
|
||
{% for item in inventory %}
|
||
<tr>
|
||
<td>{{ item.id }}</td>
|
||
<td>{{ item.market_hash_name[:35] }}{{'…' if item.market_hash_name|length > 35}}</td>
|
||
<td>{{ item.rarity }}</td>
|
||
<td>{{ "%.4f"|format(item.float_value) }}</td>
|
||
<td class="td-actions">
|
||
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteItem({{ item.id }})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block modals %}
|
||
<!-- Баланс -->
|
||
<div class="a-overlay" id="balanceModal">
|
||
<div class="a-modal">
|
||
<div class="a-modal-title">💰 Изменить баланс</div>
|
||
<p style="font-size:0.85rem;color:rgba(255,255,255,0.5);margin-bottom:1rem">
|
||
Текущий: <strong style="color:#f59e0b">{{ "%.2f"|format(target_user.balance) }} ₽</strong>
|
||
</p>
|
||
<div class="a-mb">
|
||
<label class="a-label">Сумма</label>
|
||
<input type="number" id="balanceAmount" class="a-input" min="0" step="100" value="1000">
|
||
</div>
|
||
<div class="a-mb">
|
||
<label class="a-label">Операция</label>
|
||
<select id="balanceOperation" class="a-input">
|
||
<option value="add">➕ Добавить</option>
|
||
<option value="remove">➖ Снять</option>
|
||
<option value="set">🎯 Установить</option>
|
||
</select>
|
||
</div>
|
||
<div class="a-modal-actions">
|
||
<button class="a-btn" onclick="closeModal('balanceModal')">Отмена</button>
|
||
<button class="a-btn a-btn-primary" onclick="updateBalance()">Применить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Выдать предмет -->
|
||
<div class="a-overlay" id="giveItemModal">
|
||
<div class="a-modal" style="max-width:560px">
|
||
<div class="a-modal-title">🎁 Выдать предмет</div>
|
||
<div class="a-mb">
|
||
<label class="a-label">Поиск</label>
|
||
<input type="text" id="itemSearchInput" class="a-input" placeholder="Введите название..." oninput="searchItems()">
|
||
</div>
|
||
<div id="itemSearchResults" style="max-height:300px;overflow-y:auto"></div>
|
||
<div class="a-modal-actions">
|
||
<button class="a-btn" onclick="closeModal('giveItemModal')">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RPU -->
|
||
<div class="a-overlay" id="rpuModal">
|
||
<div class="a-modal" style="max-width:500px">
|
||
<div class="a-modal-title">⚙️ Настройка РПУ</div>
|
||
<div id="rpuSliders" class="a-mb"></div>
|
||
<div class="a-mb">
|
||
<label class="a-label" style="display:flex;align-items:center;gap:0.5rem;cursor:pointer">
|
||
<input type="checkbox" id="rpuAutoAdjust"> 🔄 Автоматическая подкрутка
|
||
</label>
|
||
</div>
|
||
<div class="a-modal-actions">
|
||
<button class="a-btn" onclick="closeModal('rpuModal')">Отмена</button>
|
||
<button class="a-btn a-btn-primary" onclick="saveRPU()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_styles %}
|
||
<style>
|
||
.slider-group { margin-bottom: 0.75rem; }
|
||
.slider-group label { display:flex; justify-content:space-between; font-size:0.78rem; color:rgba(255,255,255,0.6); margin-bottom:0.2rem; }
|
||
.slider-group input[type=range] { width:100%; accent-color:#f59e0b; }
|
||
#itemSearchResults > div {
|
||
display:flex; align-items:center; gap:0.75rem;
|
||
padding:0.5rem; border-bottom:1px solid rgba(255,255,255,0.04);
|
||
}
|
||
#itemSearchResults > div img { width:40px; height:32px; object-fit:contain; }
|
||
#itemSearchResults > div .ir-info { flex:1; }
|
||
#itemSearchResults > div .ir-info .ir-name { font-size:0.82rem; }
|
||
#itemSearchResults > div .ir-info .ir-meta { font-size:0.7rem; color:rgba(255,255,255,0.35); }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
const userId = {{ target_user.id }};
|
||
let currentRPU = null;
|
||
|
||
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
||
function openModal(id) { document.getElementById(id).classList.add('open'); }
|
||
|
||
// ====== BALANCE ======
|
||
function openBalanceModal() { openModal('balanceModal'); }
|
||
async function updateBalance() {
|
||
const fd = new FormData();
|
||
fd.append('amount', document.getElementById('balanceAmount').value);
|
||
fd.append('operation', document.getElementById('balanceOperation').value);
|
||
const res = await fetch(`/admin/api/user/${userId}/balance`, { method:'POST', body:fd });
|
||
const d = await res.json();
|
||
if (d.success) { alert(d.message); window.location.reload(); }
|
||
else alert(d.error);
|
||
}
|
||
|
||
// ====== GIVE ITEM ======
|
||
function openGiveItemModal() { openModal('giveItemModal'); searchItems(); }
|
||
async function searchItems() {
|
||
const q = document.getElementById('itemSearchInput').value;
|
||
const res = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}`);
|
||
const items = await res.json();
|
||
document.getElementById('itemSearchResults').innerHTML = items.map(item => `
|
||
<div>
|
||
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||
<div class="ir-info">
|
||
<div class="ir-name">${item.name}</div>
|
||
<div class="ir-meta">${item.rarity} • ${item.price_rub || 0} ₽</div>
|
||
</div>
|
||
<button class="a-btn a-btn-primary a-btn-sm" onclick="giveItem(${item.id})">Выдать</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
async function giveItem(itemId) {
|
||
const fd = new FormData(); fd.append('item_id', itemId);
|
||
const res = await fetch(`/admin/api/user/${userId}/give-item`, { method:'POST', body:fd });
|
||
const d = await res.json();
|
||
if (d.success) { alert(d.message); closeModal('giveItemModal'); window.location.reload(); }
|
||
else alert(d.error);
|
||
}
|
||
|
||
// ====== DELETE ITEM ======
|
||
async function deleteItem(invId) {
|
||
if (!confirm('Удалить предмет?')) return;
|
||
const res = await fetch(`/admin/api/user/${userId}/delete-item/${invId}`, { method:'POST' });
|
||
const d = await res.json();
|
||
if (d.success) window.location.reload();
|
||
else alert(d.error);
|
||
}
|
||
|
||
// ====== TOGGLE ADMIN / BAN ======
|
||
async function toggleAdmin() {
|
||
if (!confirm('Изменить статус админа?')) return;
|
||
const res = await fetch(`/admin/api/user/${userId}/toggle-admin`, { method:'POST' });
|
||
const d = await res.json();
|
||
if (d.success) window.location.reload(); else alert(d.error);
|
||
}
|
||
async function toggleBan() {
|
||
if (!confirm('{{ "Забанить" if not target_user.is_banned else "Разбанить" }}?')) return;
|
||
const res = await fetch(`/admin/api/user/${userId}/toggle-ban`, { method:'POST' });
|
||
const d = await res.json();
|
||
if (d.success) window.location.reload(); else alert(d.error);
|
||
}
|
||
|
||
// ====== RPU ======
|
||
async function loadRPU() {
|
||
try {
|
||
const res = await fetch(`/admin/api/user/${userId}/rpu`);
|
||
const d = await res.json();
|
||
currentRPU = d;
|
||
const lv = Math.round(d.rpu_level);
|
||
document.getElementById('rpuLevel').textContent = lv + '%';
|
||
document.getElementById('rpuLevel').style.color = lv < 40 ? '#22c55e' : lv > 60 ? '#ef4444' : '#f59e0b';
|
||
document.getElementById('rpuLuck').textContent = d.luck_multiplier.toFixed(2) + 'x';
|
||
document.getElementById('rpuSpent').textContent = Math.round(d.stats.total_spent).toLocaleString() + ' ₽';
|
||
document.getElementById('rpuOpened').textContent = d.stats.total_opened.toLocaleString();
|
||
document.getElementById('rpuAutoStatus').textContent = d.auto_adjust ? '✅' : '❌';
|
||
} catch(_) {}
|
||
}
|
||
|
||
function openRPUModal() {
|
||
const sliders = document.getElementById('rpuSliders');
|
||
const rarities = [
|
||
['consumer','Consumer Grade'],['industrial','Industrial Grade'],
|
||
['mil_spec','Mil-Spec'],['restricted','Restricted'],
|
||
['classified','Classified'],['covert','Covert'],['rare_special','Rare Special']
|
||
];
|
||
let html = '';
|
||
rarities.forEach(([k,l]) => {
|
||
const v = currentRPU?.multipliers[k] || 1.0;
|
||
html += `<div class="slider-group"><label><span>${l}</span><span id="rp_${k}_v">${v.toFixed(2)}x</span></label>
|
||
<input type="range" id="rp_${k}" min="0.1" max="3.0" step="0.05" value="${v}"
|
||
oninput="document.getElementById('rp_${k}_v').textContent=this.value+'x'"></div>`;
|
||
});
|
||
const lv = currentRPU?.luck_multiplier || 1.0;
|
||
html += `<div class="slider-group"><label><span>🍀 Общая удача</span><span id="rp_luck_v">${lv.toFixed(2)}x</span></label>
|
||
<input type="range" id="rp_luck" min="0.1" max="3.0" step="0.05" value="${lv}"
|
||
oninput="document.getElementById('rp_luck_v').textContent=this.value+'x'"></div>`;
|
||
sliders.innerHTML = html;
|
||
document.getElementById('rpuAutoAdjust').checked = currentRPU?.auto_adjust || false;
|
||
openModal('rpuModal');
|
||
}
|
||
async function saveRPU() {
|
||
const fd = new FormData();
|
||
['consumer','industrial','mil_spec','restricted','classified','covert','rare_special'].forEach(k => fd.append(k, document.getElementById('rp_'+k).value));
|
||
fd.append('luck', document.getElementById('rp_luck').value);
|
||
fd.append('auto_adjust', document.getElementById('rpuAutoAdjust').checked);
|
||
const res = await fetch(`/admin/api/user/${userId}/rpu/update`, { method:'POST', body:fd });
|
||
const d = await res.json();
|
||
if (d.success) { alert(d.message); closeModal('rpuModal'); loadRPU(); }
|
||
else alert(d.error);
|
||
}
|
||
async function setRPUPreset(preset) {
|
||
const names = { lucky:'🍀 Везучий', normal:'📊 Обычный', unlucky:'🌧️ Невезучий', very_unlucky:'💀 Проклятый' };
|
||
if (!confirm('Применить "'+names[preset]+'"?')) return;
|
||
const fd = new FormData(); fd.append('preset', preset);
|
||
const res = await fetch(`/admin/api/user/${userId}/rpu/preset`, { method:'POST', body:fd });
|
||
const d = await res.json();
|
||
if (d.success) { alert(d.message); loadRPU(); } else alert(d.error);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', loadRPU);
|
||
</script>
|
||
{% endblock %}
|