Files
dodep-simulator/templates/admin/user_detail.html
T
root 3fe8a47e04 Dildo items, admin fixes, UI refactor
- Add 85 custom Dildo items (knife/skin pattern names, various rarities)
- Create Dildo Case config (hidden, items obtained via contracts)
- Fix admin user_detail.html nested script tag (Notify fallback removed)
- Fix admin cases.html embedded script tag
- Add notifications.js to admin base.html
- Fix admin users.html: use counts instead of relationship lists
- Replace old money format with |money filter across all templates
- Standardize nav partial (_nav.html) with active_page highlighting
- Fix RARITY_COLORS rename in activity sidebar
- Fix toast animation (forwards) and remove duplicate CSS
- Replace showToast() with Notify.* in upgrade page
- Add cs2_simulator.db and *.log to .gitignore
- Create remote-deploy.sh with DB exclusion
2026-07-05 16:15:12 +00:00

303 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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">&larr; Назад</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">{{ target_user.balance|money }} ₽</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">{{ target_user.balance|money }} ₽</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) { Notify.success(d.message); window.location.reload(); }
else Notify.error(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) { Notify.success(d.message); closeModal('giveItemModal'); window.location.reload(); }
else Notify.error(d.error);
}
// ====== DELETE ITEM ======
async function deleteItem(invId) {
Notify.confirm('Удалить предмет?', 'Подтверждение', async function(ok) {
if (!ok) 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 Notify.error(d.error);
});
}
// ====== TOGGLE ADMIN / BAN ======
async function toggleAdmin() {
Notify.confirm('Изменить статус админа?', 'Подтверждение', async function(ok) {
if (!ok) 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 Notify.error(d.error);
});
}
async function toggleBan() {
Notify.confirm('{{ "Забанить" if not target_user.is_banned else "Разбанить" }}?', 'Подтверждение', async function(ok) {
if (!ok) 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 Notify.error(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) { Notify.success(d.message); closeModal('rpuModal'); loadRPU(); }
else Notify.error(d.error);
}
async function setRPUPreset(preset) {
const names = { lucky:'🍀 Везучий', normal:'📊 Обычный', unlucky:'🌧️ Невезучий', very_unlucky:'💀 Проклятый' };
Notify.confirm('Применить "'+names[preset]+'"?', 'Подтверждение', async function(ok) {
if (!ok) 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) { Notify.success(d.message); loadRPU(); } else Notify.error(d.error);
});
}
document.addEventListener('DOMContentLoaded', loadRPU);
</script>
{% endblock %}