Files
dodep-simulator/templates/admin/cases.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

368 lines
18 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 %}Кейсы — Админ-панель{% endblock %}
{% block nav_cases %}active{% endblock %}
{% block page_title %}📦 Управление кейсами{% endblock %}
{% block top_actions %}
<button class="a-btn a-btn-primary" onclick="openCreateCaseModal()"> Создать кейс</button>
{% endblock %}
{% block content %}
<div class="a-card">
<div class="a-card-header">📂 Секции главной страницы</div>
<div id="mainpageSections">
{% for section in mainpage %}
<div class="a-card" style="margin-bottom:0.75rem;padding:1rem" data-tab="{{ section.tab }}">
<div class="a-flex" style="justify-content:space-between">
<strong style="font-size:0.9rem">{{ section.tab }}</strong>
<div class="td-actions">
<button class="a-btn a-btn-sm" onclick="openAddCaseModal('{{ section.tab }}')"> Добавить кейс</button>
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteSection('{{ section.tab }}')"></button>
</div>
</div>
<div class="a-flex" style="flex-wrap:wrap;gap:0.5rem;margin-top:0.5rem">
{% for case_name in section.cases %}
<span style="background:rgba(255,255,255,0.04);border-radius:6px;padding:0.2rem 0.5rem;font-size:0.78rem;display:flex;align-items:center;gap:0.3rem">
🎲 {{ case_name }}
<span onclick="removeCaseFromSection('{{ section.tab }}','{{ case_name }}')" style="cursor:pointer;color:#ef4444;font-size:0.7rem"></span>
</span>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<button class="a-btn a-mt" onclick="openAddSectionModal()"> Добавить секцию</button>
</div>
<div class="a-card">
<div class="a-card-header">🎲 Все кейсы</div>
<div class="a-table-wrap">
<table class="a-table">
<thead><tr><th>Название</th><th>Цена</th><th>Тип</th><th>Предметов</th><th></th></tr></thead>
<tbody>
{% for c in cases %}
<tr>
<td><strong>{{ c.name }}</strong></td>
<td>{{ c.price_open }} ₽</td>
<td style="font-size:0.75rem">{{ c.display_name|default('') }}</td>
<td>{{ c.items_count }}</td>
<td class="td-actions">
<button class="a-btn a-btn-sm" onclick="openEditCaseModal('{{ c.name }}')">✏️</button>
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteCase('{{ c.name }}')">🗑️</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block modals %}
<!-- Create/Edit Case Modal -->
<div class="a-overlay" id="caseModal">
<div class="a-modal" style="max-width:600px">
<div class="a-modal-title" id="caseModalTitle">🎲 Создать кейс</div>
<input type="hidden" id="editCaseName">
<div class="a-row a-gap-sm">
<div class="a-mb" style="flex:1">
<label class="a-label">Название кейса (ID)</label>
<input type="text" id="caseNameInput" class="a-input" placeholder="my_case">
</div>
<div class="a-mb" style="flex:1">
<label class="a-label">Отображаемое название</label>
<input type="text" id="caseDisplayName" class="a-input" placeholder="Мой кейс">
</div>
</div>
<div class="a-row a-gap-sm">
<div class="a-mb" style="flex:1">
<label class="a-label">Цена открытия</label>
<input type="number" id="casePrice" class="a-input" value="250">
</div>
<div class="a-mb" style="flex:1">
<label class="a-label">Ссылка на изображение</label>
<input type="text" id="caseImage" class="a-input" placeholder="/static/cases/default.png">
</div>
</div>
<div class="a-mb">
<label class="a-label">Описание (необязательно)</label>
<input type="text" id="caseDescription" class="a-input" placeholder="Описание кейса...">
</div>
<div class="a-card" style="background:rgba(0,0,0,0.2);padding:0.75rem">
<div class="a-card-header" style="margin-bottom:0.5rem">📋 Предметы кейса</div>
<div id="caseItemsList"></div>
<div class="a-flex a-mt" style="flex-wrap:wrap">
<input type="text" id="caseItemSearch" class="a-input a-input-sm" style="flex:1;min-width:120px" placeholder="Поиск предмета..." oninput="searchCaseItems()">
<button class="a-btn a-btn-sm" onclick="openAllItemsModal()">📦 Все предметы</button>
</div>
</div>
<div class="a-modal-actions">
<button class="a-btn" onclick="closeModal('caseModal')">Отмена</button>
<button class="a-btn a-btn-primary" id="saveCaseBtn" onclick="saveCase()">💾 Сохранить</button>
</div>
</div>
</div>
<!-- All items browser modal -->
<div class="a-overlay" id="allItemsModal">
<div class="a-modal" style="max-width:700px;max-height:80vh;display:flex;flex-direction:column">
<div class="a-modal-title">📦 Все предметы</div>
<input type="text" id="allItemsSearch" class="a-input a-input-sm a-mb" placeholder="Поиск..." oninput="searchAllItems()">
<div id="allItemsGrid" style="flex:1;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:0.5rem"></div>
</div>
</div>
<!-- Add case to section modal -->
<div class="a-overlay" id="addCaseModal">
<div class="a-modal" style="max-width:400px">
<div class="a-modal-title">➕ Добавить кейс в секцию</div>
<p style="font-size:0.82rem;color:rgba(255,255,255,0.5);margin-bottom:0.75rem">Секция: <strong id="addCaseSectionName"></strong></p>
<div class="a-mb">
<label class="a-label">Выберите кейс</label>
<select id="addCaseSelect" class="a-input"></select>
</div>
<div class="a-modal-actions">
<button class="a-btn" onclick="closeModal('addCaseModal')">Отмена</button>
<button class="a-btn a-btn-primary" onclick="confirmAddCase()">Добавить</button>
</div>
</div>
</div>
<!-- Add section modal -->
<div class="a-overlay" id="addSectionModal">
<div class="a-modal" style="max-width:400px">
<div class="a-modal-title"> Новая секция</div>
<div class="a-mb">
<label class="a-label">Название вкладки</label>
<input type="text" id="newSectionName" class="a-input" placeholder="Новая коллекция">
</div>
<div class="a-modal-actions">
<button class="a-btn" onclick="closeModal('addSectionModal')">Отмена</button>
<button class="a-btn a-btn-primary" onclick="confirmAddSection()">Создать</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_styles %}
<style>
.ci-item { display:flex; align-items:center; gap:0.5rem; padding:0.35rem 0; border-bottom:1px solid rgba(255,255,255,0.03); font-size:0.78rem; }
.ci-item img { width:32px; height:24px; object-fit:contain; }
.ci-item .ci-info { flex:1; }
.ci-item .ci-rarity { font-size:0.65rem; color:rgba(255,255,255,0.35); }
.all-item-card { background:rgba(0,0,0,0.15); border-radius:6px; padding:0.35rem; cursor:pointer; text-align:center; font-size:0.7rem; transition:all 0.15s; border:1px solid transparent; }
.all-item-card:hover { border-color:rgba(245,158,11,0.3); background:rgba(245,158,11,0.05); }
.all-item-card img { width:100%; height:40px; object-fit:contain; }
</style>
{% endblock %}
{% block scripts %}
<script>
let currentCaseItems = [];
let editingCaseName = null;
// ─── Modals ──────────────────────────────────────────────────────────────────
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
function openModal(id) { document.getElementById(id).classList.add('open'); }
// ─── Load page data ──────────────────────────────────────────────────────────
let allCasesList = [];
document.addEventListener('DOMContentLoaded', async () => {
const r = await fetch('/admin/api/cases/list');
const d = await r.json();
allCasesList = d.cases || [];
});
// ─── Create / Edit Case ──────────────────────────────────────────────────────
function openCreateCaseModal() {
editingCaseName = null;
document.getElementById('caseModalTitle').textContent = '🎲 Создать кейс';
document.getElementById('saveCaseBtn').textContent = '💾 Создать';
document.getElementById('editCaseName').value = '';
document.getElementById('caseNameInput').value = '';
document.getElementById('caseDisplayName').value = '';
document.getElementById('casePrice').value = '250';
document.getElementById('caseImage').value = '';
document.getElementById('caseDescription').value = '';
currentCaseItems = [];
renderCaseItems();
openModal('caseModal');
}
function openEditCaseModal(caseName) {
editingCaseName = caseName;
document.getElementById('caseModalTitle').textContent = '✏️ Редактировать кейс';
document.getElementById('saveCaseBtn').textContent = '💾 Сохранить';
document.getElementById('editCaseName').value = caseName;
const cfg = {{ cases_config|tojson }};
const c = cfg[caseName] || {};
document.getElementById('caseNameInput').value = caseName;
document.getElementById('caseDisplayName').value = c.display_name || '';
document.getElementById('casePrice').value = c.price_open || 250;
document.getElementById('caseImage').value = c.image_url || '';
document.getElementById('caseDescription').value = c.description || '';
currentCaseItems = (c.items || []).filter(i => typeof i === 'object');
renderCaseItems();
openModal('caseModal');
}
function renderCaseItems() {
const div = document.getElementById('caseItemsList');
div.innerHTML = currentCaseItems.map((item, i) => `
<div class="ci-item">
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
<div class="ci-info">
<div>${item.market_hash_name || item.name || '???'}</div>
<div class="ci-rarity">${item.rarity || ''}${item.weapon || ''}</div>
</div>
<button class="a-btn a-btn-danger a-btn-sm" onclick="removeCaseItem(${i})">✕</button>
</div>
`).join('');
}
function removeCaseItem(idx) {
currentCaseItems.splice(idx, 1);
renderCaseItems();
}
function addCaseItem(item) {
if (currentCaseItems.some(i => (i.market_hash_name || i.name) === (item.market_hash_name || item.name))) return;
currentCaseItems.push(item);
renderCaseItems();
}
async function searchCaseItems() {
const q = document.getElementById('caseItemSearch').value;
if (q.length < 2) return;
const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}`);
const items = await r.json();
// show as dropdown
let html = '<div style="margin-top:0.5rem;max-height:200px;overflow-y:auto">';
items.forEach(item => {
html += `<div class="ci-item" style="cursor:pointer" onclick="addCaseItem(${JSON.stringify(item).replace(/"/g,'&quot;')})">
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
<div class="ci-info"><div>${item.name}</div><div class="ci-rarity">${item.rarity}${item.price_rub||0} ₽</div></div>
</div>`;
});
html += '</div>';
const existing = document.getElementById('caseItemSearchResults');
if (!existing) {
const d = document.createElement('div'); d.id = 'caseItemSearchResults';
document.getElementById('caseItemSearch').parentNode.appendChild(d);
}
document.getElementById('caseItemSearchResults').innerHTML = html;
}
async function saveCase() {
const name = document.getElementById('caseNameInput').value.trim();
if (!name) { Notify.error('Введите название кейса'); return; }
const fd = new FormData();
fd.append('case_name', name);
fd.append('display_name', document.getElementById('caseDisplayName').value);
fd.append('price_open', document.getElementById('casePrice').value);
fd.append('image_url', document.getElementById('caseImage').value);
fd.append('description', document.getElementById('caseDescription').value);
fd.append('items_json', JSON.stringify(currentCaseItems));
const isEdit = editingCaseName !== null && editingCaseName !== name;
const url = isEdit
? `/admin/api/cases/update/${encodeURIComponent(editingCaseName)}`
: '/admin/api/cases/create';
const r = await fetch(url, { method:'POST', body:fd });
const d = await r.json();
if (d.success) { window.location.reload(); }
else Notify.error(d.error || 'Ошибка');
}
async function deleteCase(name) {
Notify.confirm(`Удалить кейс "${name}"?`, 'Подтверждение', async function(ok) {
if (!ok) return;
const fd = new FormData(); fd.append('case_name', name);
const r = await fetch(`/admin/api/cases/delete/${encodeURIComponent(name)}`, { method:'POST', body:fd });
const d = await r.json();
if (d.success) window.location.reload();
else Notify.error(d.error);
});
}
// ─── All Items Browser ───────────────────────────────────────────────────────
async function openAllItemsModal() {
openModal('allItemsModal');
document.getElementById('allItemsSearch').value = '';
await searchAllItems();
}
async function searchAllItems() {
const q = document.getElementById('allItemsSearch').value;
const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}&limit=200`);
const items = await r.json();
document.getElementById('allItemsGrid').innerHTML = items.map(item => `
<div class="all-item-card" onclick="addCaseItemFromAll(${JSON.stringify(item).replace(/"/g,'&quot;')})">
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
<div style="margin-top:0.2rem;line-height:1.15">${item.name}</div>
<div style="font-size:0.6rem;color:rgba(255,255,255,0.35)">${item.rarity}</div>
</div>
`).join('');
}
function addCaseItemFromAll(item) {
addCaseItem(item);
closeModal('allItemsModal');
}
// ─── Mainpage Sections ───────────────────────────────────────────────────────
function openAddSectionModal() {
document.getElementById('newSectionName').value = '';
openModal('addSectionModal');
}
async function confirmAddSection() {
const name = document.getElementById('newSectionName').value.trim();
if (!name) return;
const fd = new FormData(); fd.append('tab', name);
const r = await fetch('/admin/api/mainpage/section/add', { method:'POST', body:fd });
const d = await r.json();
if (d.success) window.location.reload();
else Notify.error(d.error);
}
async function deleteSection(tab) {
Notify.confirm(`Удалить секцию "${tab}"?`, 'Подтверждение', async function(ok) {
if (!ok) return;
const fd = new FormData(); fd.append('tab', tab);
const r = await fetch('/admin/api/mainpage/section/delete', { method:'POST', body:fd });
const d = await r.json();
if (d.success) window.location.reload();
else Notify.error(d.error);
});
}
function openAddCaseModal(tab) {
document.getElementById('addCaseSectionName').textContent = tab;
const sel = document.getElementById('addCaseSelect');
sel.innerHTML = allCasesList.map(c => `<option value="${c}">${c}</option>`).join('');
sel.dataset.tab = tab;
openModal('addCaseModal');
}
async function confirmAddCase() {
const tab = document.getElementById('addCaseSelect').dataset.tab;
const caseName = document.getElementById('addCaseSelect').value;
const fd = new FormData();
fd.append('tab', tab);
fd.append('case_name', caseName);
const r = await fetch('/admin/api/mainpage/section/add-case', { method:'POST', body:fd });
const d = await r.json();
if (d.success) window.location.reload();
else Notify.error(d.error);
}
async function removeCaseFromSection(tab, caseName) {
Notify.confirm(`Убрать "${caseName}" из "${tab}"?`, 'Подтверждение', async function(ok) {
if (!ok) return;
const fd = new FormData();
fd.append('tab', tab);
fd.append('case_name', caseName);
const r = await fetch('/admin/api/mainpage/section/remove-case', { method:'POST', body:fd });
const d = await r.json();
if (d.success) window.location.reload();
else Notify.error(d.error);
});
}
</script>
{% endblock %}