| Current Path : /home/seto/888mirror.pm/admin/ |
| Current File : /home/seto/888mirror.pm/admin/partners.php |
<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/helpers.php';
auth_check();
$db = db();
$action = $_POST['action'] ?? '';
if ($action === 'save') {
$pid = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$type = in_array($_POST['type'] ?? '', ['direct','network']) ? $_POST['type'] : 'direct';
$cname = trim($_POST['contact_name'] ?? '');
$ctg = trim($_POST['contact_telegram'] ?? '');
$aurl = trim($_POST['affiliate_url'] ?? '');
$notes = trim($_POST['notes'] ?? '');
if ($name) {
if ($pid) {
$db->prepare("UPDATE partners SET name=?,type=?,contact_name=?,contact_telegram=?,affiliate_url=?,notes=? WHERE id=?")
->execute([$name,$type,$cname?:null,$ctg?:null,$aurl?:null,$notes?:null,$pid]);
} else {
$db->prepare("INSERT INTO partners (name,type,contact_name,contact_telegram,affiliate_url,notes) VALUES (?,?,?,?,?,?)")
->execute([$name,$type,$cname?:null,$ctg?:null,$aurl?:null,$notes?:null]);
}
}
header('Location: /admin/partners.php?msg=' . urlencode('Сохранено'));
exit();
}
if ($action === 'delete') {
$pid = (int)($_POST['id'] ?? 0);
if ($pid) {
$db->prepare("UPDATE brands SET partner_id = NULL WHERE partner_id = ?")->execute([$pid]);
$db->prepare("DELETE FROM partners WHERE id = ?")->execute([$pid]);
}
header('Location: /admin/partners.php?msg=' . urlencode('Партнёрка удалена'));
exit();
}
if ($action === 'assign') {
$brand_id = (int)($_POST['brand_id'] ?? 0);
$partner_id = (int)($_POST['partner_id'] ?? 0);
if ($brand_id && $partner_id) {
$db->prepare("UPDATE brands SET partner_id = ? WHERE id = ?")->execute([$partner_id, $brand_id]);
}
header('Location: /admin/partners.php');
exit();
}
if ($action === 'unassign') {
$brand_id = (int)($_POST['brand_id'] ?? 0);
if ($brand_id) {
$db->prepare("UPDATE brands SET partner_id = NULL WHERE id = ?")->execute([$brand_id]);
}
header('Location: /admin/partners.php');
exit();
}
// Данные
$partners = $db->query("SELECT * FROM partners ORDER BY name ASC")->fetchAll();
$all_brands = $db->query("
SELECT id, name, partner_id, payment_models, rate_cpa, rate_revshare
FROM brands WHERE status != 'inactive'
ORDER BY name ASC
")->fetchAll();
$brands_by_partner = [];
$unassigned_brands = [];
foreach ($all_brands as $b) {
if ($b['partner_id']) {
$brands_by_partner[(int)$b['partner_id']][] = $b;
} else {
$unassigned_brands[] = $b;
}
}
// Уникальных GEO на партнёрку (исключаем виртуальные en/crypto)
$geo_raw = $db->query("
SELECT b.partner_id, COUNT(DISTINCT bc.country_code) as geo_count
FROM brand_countries bc
JOIN brands b ON b.id = bc.brand_id
WHERE b.partner_id IS NOT NULL
AND b.status != 'inactive'
AND bc.country_code NOT IN ('en','crypto')
GROUP BY b.partner_id
")->fetchAll();
$geo_by_partner = array_column($geo_raw, 'geo_count', 'partner_id');
function models_str(array $b): string {
$models = json_decode($b['payment_models'] ?? '[]', true) ?: [];
$parts = [];
if (in_array('cpa', $models) && $b['rate_cpa'] !== null) $parts[] = 'CPA ' . (int)$b['rate_cpa'];
if (in_array('revshare', $models) && $b['rate_revshare'] !== null) $parts[] = 'RS ' . $b['rate_revshare'] . '%';
if (in_array('hybrid', $models)) $parts[] = 'Hybrid';
return implode(' · ', $parts);
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Партнёрки — Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
.partner-card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,.08);
transition: transform .15s, box-shadow .15s;
overflow: hidden;
}
.partner-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0,0,0,.14);
}
.partner-card .card-header {
border-bottom: 1px solid rgba(0,0,0,.06);
padding: .75rem 1rem;
}
.card-accent { height: 5px; width: 100%; }
.contact-block {
background: rgba(0,0,0,.03);
border-radius: 8px;
padding: .5rem .75rem;
margin-bottom: .75rem;
}
.brand-row {
background: rgba(0,0,0,.02);
border-radius: 6px;
padding: .25rem .5rem;
margin-bottom: .3rem;
}
.brand-row:hover { background: rgba(0,0,0,.05); }
.pp-icon {
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1rem; flex-shrink: 0;
}
</style>
</head>
<body>
<?= admin_nav('partners') ?>
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">Партнёрки</h4>
<div class="d-flex gap-2">
<input type="text" id="ppSearch" class="form-control form-control-sm" placeholder="Поиск по названию..." style="width:220px" oninput="filterCards(this.value)">
<button class="btn btn-primary btn-sm" onclick="openModal()">+ Добавить ПП</button>
</div>
</div>
<?php if (isset($_GET['msg'])): ?>
<div class="alert alert-success alert-dismissible fade show py-2">
<?= h($_GET['msg']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif ?>
<?php if (empty($partners)): ?>
<div class="text-center text-muted py-5">
<p>Партнёрок пока нет.</p>
<button class="btn btn-primary" onclick="openModal()">+ Добавить первую</button>
</div>
<?php else: ?>
<?php
$palette = [
['#6366f1','#eef2ff','#818cf8'], // indigo
['#ec4899','#fdf2f8','#f472b6'], // pink
['#f59e0b','#fffbeb','#fbbf24'], // amber
['#10b981','#ecfdf5','#34d399'], // emerald
['#3b82f6','#eff6ff','#60a5fa'], // blue
['#ef4444','#fef2f2','#f87171'], // red
['#8b5cf6','#f5f3ff','#a78bfa'], // violet
['#06b6d4','#ecfeff','#22d3ee'], // cyan
];
?>
<div class="row g-3 mb-4">
<?php foreach ($partners as $idx => $p):
$p_brands = $brands_by_partner[(int)$p['id']] ?? [];
$type_label = $p['type'] === 'network' ? 'Network' : 'Прямой рекл.';
$type_icon = $p['type'] === 'network' ? 'bi-diagram-3-fill' : 'bi-person-badge-fill';
$available = $unassigned_brands;
[$accent, $bg_light, $accent_light] = $palette[$idx % count($palette)];
$geo_count = (int)($geo_by_partner[$p['id']] ?? 0);
?>
<div class="col-md-4 pp-card-col" data-name="<?= h(mb_strtolower($p['name'])) ?>">
<div class="card partner-card h-100">
<div class="card-accent" style="background: linear-gradient(90deg, <?= $accent ?>, <?= $accent_light ?>)"></div>
<div class="card-header d-flex align-items-center gap-2" style="background:<?= $bg_light ?>">
<div class="pp-icon" style="background:<?= $accent ?>22; color:<?= $accent ?>">
<i class="bi <?= $type_icon ?>"></i>
</div>
<div class="flex-grow-1 overflow-hidden">
<div class="fw-bold text-truncate"><?= h($p['name']) ?></div>
<div class="small" style="color:<?= $accent ?>"><?= $type_label ?></div>
</div>
<div class="d-flex flex-column align-items-end gap-1">
<?php if (count($p_brands) === 0): ?>
<span class="badge rounded-pill bg-danger">
<i class="bi bi-exclamation-triangle-fill me-1"></i>0 брендов
</span>
<?php else: ?>
<span class="badge rounded-pill" style="background:<?= $accent ?>22;color:<?= $accent ?>;border:1px solid <?= $accent ?>44">
<i class="bi bi-grid-3x3-gap-fill me-1"></i><?= count($p_brands) ?> брендов
</span>
<?php endif ?>
<span class="badge rounded-pill" style="background:<?= $accent ?>22;color:<?= $accent ?>;border:1px solid <?= $accent ?>44">
<i class="bi bi-geo-alt-fill me-1"></i><?= $geo_count ?> GEO
</span>
</div>
</div>
<div class="card-body py-3">
<?php if ($p['contact_name'] || $p['contact_telegram'] || $p['affiliate_url']): ?>
<div class="contact-block small">
<?php if ($p['contact_name']): ?>
<div class="mb-1"><i class="bi bi-person me-1" style="color:<?= $accent ?>"></i><strong><?= h($p['contact_name']) ?></strong></div>
<?php endif ?>
<?php if ($p['contact_telegram']): ?>
<div class="mb-1">
<i class="bi bi-telegram me-1" style="color:<?= $accent ?>"></i>
<a href="https://t.me/<?= h(ltrim($p['contact_telegram'], '@')) ?>" target="_blank" rel="noopener" style="color:<?= $accent ?>">
<?= h($p['contact_telegram']) ?>
</a>
</div>
<?php endif ?>
<?php if ($p['affiliate_url']): ?>
<div class="text-truncate">
<i class="bi bi-link-45deg me-1" style="color:<?= $accent ?>"></i>
<a href="<?= h($p['affiliate_url']) ?>" target="_blank" rel="noopener" class="text-muted">
<?= h(preg_replace('#^https?://#', '', $p['affiliate_url'])) ?>
</a>
</div>
<?php endif ?>
</div>
<?php endif ?>
<?php if ($p['notes']): ?>
<div class="small text-muted fst-italic mb-2"><?= h($p['notes']) ?></div>
<?php endif ?>
<div class="small fw-semibold mb-2" style="color:<?= $accent ?>">
<i class="bi bi-grid me-1"></i>Бренды
</div>
<?php foreach ($p_brands as $b): ?>
<?php $ms = models_str($b); ?>
<div class="brand-row d-flex justify-content-between align-items-center">
<span class="text-truncate me-1">
<a href="/admin/brand_edit.php?id=<?= $b['id'] ?>" class="text-decoration-none small fw-medium">
<?= h($b['name']) ?>
</a>
<?php if ($ms): ?>
<span class="badge ms-1" style="background:<?= $accent ?>22;color:<?= $accent ?>;font-size:.68rem"><?= h($ms) ?></span>
<?php endif ?>
</span>
<form method="post" class="flex-shrink-0">
<input type="hidden" name="action" value="unassign">
<input type="hidden" name="brand_id" value="<?= $b['id'] ?>">
<button class="btn btn-link btn-sm p-0 text-danger lh-1" title="Отвязать"
onclick="return confirm('Отвязать «<?= h(addslashes($b['name'])) ?>» от ПП?')">
<i class="bi bi-x-lg" style="font-size:.7rem"></i>
</button>
</form>
</div>
<?php endforeach ?>
<?php if (!empty($available)): ?>
<form method="post" class="mt-2 d-flex gap-1">
<input type="hidden" name="action" value="assign">
<input type="hidden" name="partner_id" value="<?= $p['id'] ?>">
<select name="brand_id" class="form-select form-select-sm">
<option value="">+ Привязать бренд...</option>
<?php foreach ($available as $ab): ?>
<option value="<?= $ab['id'] ?>"><?= h($ab['name']) ?></option>
<?php endforeach ?>
</select>
<button class="btn btn-sm flex-shrink-0" style="background:<?= $accent ?>;color:#fff;border:none"
onclick="return this.form.brand_id.value !== ''">Ок</button>
</form>
<?php endif ?>
</div>
<div class="card-footer py-2 d-flex gap-2" style="background:<?= $bg_light ?>;border-top:1px solid <?= $accent ?>22">
<button class="btn btn-sm flex-fill fw-medium"
style="background:<?= $accent ?>15;color:<?= $accent ?>;border:1px solid <?= $accent ?>33"
onclick="openModal(<?= htmlspecialchars(json_encode($p), ENT_QUOTES) ?>)">
<i class="bi bi-pencil"></i> Ред.
</button>
<form method="post" class="flex-fill" onsubmit="return confirm('Удалить «<?= h(addslashes($p['name'])) ?>»?')">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $p['id'] ?>">
<button class="btn btn-sm btn-outline-danger w-100"><i class="bi bi-trash"></i> Удалить</button>
</form>
</div>
</div>
</div>
<?php endforeach ?>
</div>
<div id="ppNoResults" class="text-center text-muted py-4" style="display:none">Ничего не найдено</div>
<?php endif ?>
<?php if (!empty($unassigned_brands)): ?>
<div class="card mb-4 border-warning">
<div class="card-header bg-warning bg-opacity-10 fw-semibold">
Бренды без ПП (<?= count($unassigned_brands) ?>)
</div>
<div class="card-body p-2">
<div class="d-flex flex-wrap gap-2">
<?php foreach ($unassigned_brands as $b): ?>
<a href="/admin/brand_edit.php?id=<?= $b['id'] ?>" class="badge bg-secondary text-decoration-none">
<?= h($b['name']) ?>
</a>
<?php endforeach ?>
</div>
</div>
</div>
<?php endif ?>
</div>
<!-- Modal: создание / редактирование ПП -->
<div class="modal fade" id="partnerModal" tabindex="-1">
<div class="modal-dialog">
<form method="post" class="modal-content">
<input type="hidden" name="action" value="save">
<input type="hidden" name="id" id="modal_id" value="0">
<div class="modal-header py-2">
<h6 class="modal-title mb-0" id="modalTitle">Новая партнёрка</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-8">
<label class="form-label">Название ПП *</label>
<input type="text" name="name" id="modal_name" class="form-control" required placeholder="Имя партнёрки">
</div>
<div class="col-4">
<label class="form-label">Тип</label>
<select name="type" id="modal_type" class="form-select">
<option value="direct">Прямой рекл.</option>
<option value="network">Network</option>
</select>
</div>
<div class="col-6">
<label class="form-label">Имя контакта</label>
<input type="text" name="contact_name" id="modal_cname" class="form-control" placeholder="Иван Иванов">
</div>
<div class="col-6">
<label class="form-label">Telegram</label>
<input type="text" name="contact_telegram" id="modal_ctg" class="form-control" placeholder="@username">
</div>
<div class="col-12">
<label class="form-label">Адрес платформы</label>
<input type="url" name="affiliate_url" id="modal_aurl" class="form-control" placeholder="https://...">
</div>
<div class="col-12">
<label class="form-label">Заметки</label>
<textarea name="notes" id="modal_notes" class="form-control" rows="2" placeholder="Доп. информация"></textarea>
</div>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary btn-sm">Сохранить</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
var partnerModal = new bootstrap.Modal(document.getElementById('partnerModal'));
function filterCards(q) {
var q2 = q.trim().toLowerCase();
var empty = true;
document.querySelectorAll('.pp-card-col').forEach(function(col) {
var match = !q2 || col.dataset.name.indexOf(q2) !== -1;
col.style.display = match ? '' : 'none';
if (match) empty = false;
});
var hint = document.getElementById('ppNoResults');
if (hint) hint.style.display = empty ? '' : 'none';
}
function openModal(p) {
document.getElementById('modal_id').value = p ? p.id : 0;
document.getElementById('modal_name').value = p ? p.name : '';
document.getElementById('modal_type').value = p ? p.type : 'direct';
document.getElementById('modal_cname').value = p ? (p.contact_name || '') : '';
document.getElementById('modal_ctg').value = p ? (p.contact_telegram || '') : '';
document.getElementById('modal_aurl').value = p ? (p.affiliate_url || '') : '';
document.getElementById('modal_notes').value = p ? (p.notes || '') : '';
document.getElementById('modalTitle').textContent = p ? 'Редактировать ПП' : 'Новая партнёрка';
partnerModal.show();
}
</script>
</body>
</html>