Compare commits

..

No commits in common. "ai-dev" and "main" have entirely different histories.
ai-dev ... main

52 changed files with 811 additions and 43758 deletions

View File

View File

727
admin.php
View File

@ -1,727 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
if (!auth_is_logged_in()) {
header('Location: index.php');
exit;
}
$is_admin = auth_is_admin();
$current_role = auth_current_role();
$current_role_label = auth_role_label($current_role);
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$edit_cl_auth_id = isset($_GET['edit']) ? (int) $_GET['edit'] : 0;
$edit_cl_auth_user = '';
$edit_cl_auth_right = 'member';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$csrf_token = isset($_POST['csrf_token']) ? (string) $_POST['csrf_token'] : null;
if (!auth_validate_csrf($csrf_token)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: admin.php');
exit;
}
$admin_action = (string) ($_POST['admin_action'] ?? '');
if (!$is_admin) {
auth_flash_set('error', 'Seul un administrateur peut gérer les utilisateurs.');
header('Location: admin.php');
exit;
}
if ($admin_action === 'create') {
$submitted_cl_auth_user = trim((string) ($_POST['cl_auth_user'] ?? ''));
$submitted_cl_auth_pass = (string) ($_POST['cl_auth_pass'] ?? '');
$submitted_cl_auth_right = (string) ($_POST['cl_auth_right'] ?? 'member');
if ($submitted_cl_auth_user === '' || $submitted_cl_auth_pass === '') {
auth_flash_set('error', 'Le login et le mot de passe sont obligatoires.');
header('Location: admin.php');
exit;
}
if (!in_array($submitted_cl_auth_right, auth_valid_roles(), true)) {
auth_flash_set('error', 'Droit utilisateur invalide.');
header('Location: admin.php');
exit;
}
$stmt_duplicate_user = db()->prepare('SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_user = :cl_auth_user');
$stmt_duplicate_user->execute([
'cl_auth_user' => $submitted_cl_auth_user,
]);
$cl_auth_user_total = (int) $stmt_duplicate_user->fetchColumn();
if ($cl_auth_user_total > 0) {
auth_flash_set('error', 'Ce login existe déjà.');
header('Location: admin.php');
exit;
}
$cl_auth_user = $submitted_cl_auth_user;
$cl_auth_pass = password_hash($submitted_cl_auth_pass, PASSWORD_DEFAULT);
$cl_auth_right = $submitted_cl_auth_right;
$stmt_create_user = db()->prepare(
'INSERT INTO tbl_auth (cl_auth_user, cl_auth_pass, cl_auth_right) VALUES (:cl_auth_user, :cl_auth_pass, :cl_auth_right)'
);
$stmt_create_user->execute([
'cl_auth_user' => $cl_auth_user,
'cl_auth_pass' => $cl_auth_pass,
'cl_auth_right' => $cl_auth_right,
]);
auth_flash_set('success', 'Compte créé avec succès.');
header('Location: admin.php');
exit;
}
if ($admin_action === 'update') {
$cl_auth_id = (int) ($_POST['cl_auth_id'] ?? 0);
$submitted_cl_auth_user = trim((string) ($_POST['cl_auth_user'] ?? ''));
$submitted_cl_auth_pass = (string) ($_POST['cl_auth_pass'] ?? '');
$submitted_cl_auth_right = (string) ($_POST['cl_auth_right'] ?? 'member');
if ($cl_auth_id <= 0 || $submitted_cl_auth_user === '') {
auth_flash_set('error', 'Données de modification invalides.');
header('Location: admin.php');
exit;
}
if (!in_array($submitted_cl_auth_right, auth_valid_roles(), true)) {
auth_flash_set('error', 'Droit utilisateur invalide.');
header('Location: admin.php?edit=' . $cl_auth_id);
exit;
}
$stmt_tbl_auth = db()->prepare('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth WHERE cl_auth_id = :cl_auth_id LIMIT 1');
$stmt_tbl_auth->execute([
'cl_auth_id' => $cl_auth_id,
]);
$tbl_auth = $stmt_tbl_auth->fetch();
if (!$tbl_auth) {
auth_flash_set('error', 'Utilisateur introuvable.');
header('Location: admin.php');
exit;
}
$current_cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$current_cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$current_cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$current_cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($current_cl_auth_id, $current_cl_auth_user);
$stmt_duplicate_user = db()->prepare(
'SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_user = :cl_auth_user AND cl_auth_id <> :cl_auth_id'
);
$stmt_duplicate_user->execute([
'cl_auth_user' => $submitted_cl_auth_user,
'cl_auth_id' => $cl_auth_id,
]);
$cl_auth_user_total = (int) $stmt_duplicate_user->fetchColumn();
if ($cl_auth_user_total > 0) {
auth_flash_set('error', 'Ce login existe déjà.');
header('Location: admin.php?edit=' . $cl_auth_id);
exit;
}
if ($current_cl_auth_right === 'admin' && $submitted_cl_auth_right !== 'admin') {
$stmt_admin_total = db()->query("SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_right = 'admin'");
$cl_auth_admin_total = (int) $stmt_admin_total->fetchColumn();
if ($cl_auth_admin_total <= 1) {
auth_flash_set('error', 'Impossible de rétrograder le dernier administrateur.');
header('Location: admin.php?edit=' . $cl_auth_id);
exit;
}
}
$cl_auth_user = $submitted_cl_auth_user;
$cl_auth_right = $submitted_cl_auth_right;
$cl_auth_pass = $current_cl_auth_pass;
if ($submitted_cl_auth_pass !== '') {
$cl_auth_pass = password_hash($submitted_cl_auth_pass, PASSWORD_DEFAULT);
}
$stmt_update_user = db()->prepare(
'UPDATE tbl_auth
SET cl_auth_user = :cl_auth_user,
cl_auth_pass = :cl_auth_pass,
cl_auth_right = :cl_auth_right
WHERE cl_auth_id = :cl_auth_id'
);
$stmt_update_user->execute([
'cl_auth_user' => $cl_auth_user,
'cl_auth_pass' => $cl_auth_pass,
'cl_auth_right' => $cl_auth_right,
'cl_auth_id' => $cl_auth_id,
]);
if (isset($_SESSION['user']) && $_SESSION['user'] === $tbl_auth['cl_auth_user']) {
$_SESSION['user'] = $cl_auth_user;
$_SESSION['role'] = $cl_auth_right;
}
auth_flash_set('success', 'Compte modifié avec succès.');
header('Location: admin.php');
exit;
}
if ($admin_action === 'delete') {
$cl_auth_id = (int) ($_POST['cl_auth_id'] ?? 0);
if ($cl_auth_id <= 0) {
auth_flash_set('error', 'Suppression impossible.');
header('Location: admin.php');
exit;
}
$stmt_tbl_auth = db()->prepare('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth WHERE cl_auth_id = :cl_auth_id LIMIT 1');
$stmt_tbl_auth->execute([
'cl_auth_id' => $cl_auth_id,
]);
$tbl_auth = $stmt_tbl_auth->fetch();
if (!$tbl_auth) {
auth_flash_set('error', 'Utilisateur introuvable.');
header('Location: admin.php');
exit;
}
$cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($cl_auth_pass);
if ($cl_auth_right === 'admin') {
$stmt_admin_total = db()->query("SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_right = 'admin'");
$cl_auth_admin_total = (int) $stmt_admin_total->fetchColumn();
if ($cl_auth_admin_total <= 1) {
auth_flash_set('error', 'Impossible de supprimer le dernier administrateur.');
header('Location: admin.php');
exit;
}
}
$stmt_delete_user = db()->prepare('DELETE FROM tbl_auth WHERE cl_auth_id = :cl_auth_id');
$stmt_delete_user->execute([
'cl_auth_id' => $cl_auth_id,
]);
if (isset($_SESSION['user']) && $_SESSION['user'] === $cl_auth_user) {
header('Location: logout.php');
exit;
}
auth_flash_set('success', 'Compte supprimé avec succès.');
header('Location: admin.php');
exit;
}
}
if ($is_admin && $edit_cl_auth_id > 0) {
$stmt_edit_user = db()->prepare('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth WHERE cl_auth_id = :cl_auth_id LIMIT 1');
$stmt_edit_user->execute([
'cl_auth_id' => $edit_cl_auth_id,
]);
$tbl_auth = $stmt_edit_user->fetch();
if ($tbl_auth) {
$edit_cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$edit_cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$edit_cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$edit_cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($edit_cl_auth_pass);
} else {
$edit_cl_auth_id = 0;
auth_flash_set('error', 'Utilisateur introuvable.');
header('Location: admin.php');
exit;
}
}
$tbl_auth_all = [];
if ($is_admin) {
$stmt_users = db()->query('SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right FROM tbl_auth ORDER BY cl_auth_user ASC');
$tbl_auth_all = $stmt_users->fetchAll();
}
$user_accessible_items = [];
if (!$is_admin) {
foreach (auth_navigation_items() as $item) {
$file = (string) ($item['file'] ?? '');
$label = (string) ($item['label'] ?? $file);
$admin_only = !empty($item['admin_only']);
if ($admin_only) {
continue;
}
if (auth_user_can_access_page($file, $label)) {
$user_accessible_items[] = [
'file' => $file,
'label' => $label,
];
}
}
}
$csrf_token = auth_csrf_token();
[$default_admin_user, $default_admin_password] = auth_default_admin_credentials();
$current_session_user = isset($_SESSION['user']) ? (string) $_SESSION['user'] : '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Administration Sécure | R.E.A.C.T.</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.topbar-actions {
display: flex;
gap: 1rem;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger {
border-color: var(--danger);
color: var(--danger);
}
.btn-modern.danger:hover {
background: var(--danger);
color: #fff;
box-shadow: 0 0 15px rgba(255, 77, 77, 0.3);
}
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-content { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #aaa;
text-transform: uppercase;
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.5);
}
select.form-control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.form-control:focus {
background: #3d444f;
color: #fff;
}
select.form-control option {
background: #353b45;
color: #fff;
}
select.form-control option:checked {
background: #4a5260;
color: #fff;
}
.modern-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
}
.modern-table th {
text-align: left;
padding: 1rem;
font-size: 0.8rem;
text-transform: uppercase;
color: var(--primary);
opacity: 0.7;
}
.modern-table td {
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
border-top: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.modern-table td:first-child {
border-left: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px 0 0 8px;
}
.modern-table td:last-child {
border-right: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 0 8px 8px 0;
}
.modern-table tr:hover td {
background: rgba(162, 155, 120, 0.05);
}
.badge {
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
text-transform: uppercase;
}
.badge-admin { background: rgba(162, 155, 120, 0.2); color: var(--primary); border: 1px solid var(--primary); }
.badge-moderator { background: rgba(74, 144, 226, 0.16); color: #8fc7ff; border: 1px solid rgba(143, 199, 255, 0.6); }
.badge-member { background: rgba(255, 255, 255, 0.1); color: #ccc; border: 1px solid #555; }
.flash {
padding: 1rem 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
font-size: 0.9rem;
border-left: 4px solid var(--primary);
background: rgba(162, 155, 120, 0.1);
}
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.row-actions {
display: flex;
gap: 0.5rem;
}
.btn-mini {
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
}
.user-id {
font-family: monospace;
color: var(--primary);
font-weight: bold;
}
.empty-state {
padding: 2rem;
text-align: center;
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Core Admin</h1>
<p>Niveau d'accès : <strong><?php echo $is_admin ? 'Administrateur' : 'Membre'; ?></strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php if ($is_admin): ?>
<?php echo auth_render_app_nav('admin.php'); ?>
<?php else: ?>
<nav class="nav-tabs">
<a href="admin.php" class="active">Zone admin</a>
<?php foreach ($member_accessible_items as $item): ?>
<a href="<?php echo htmlspecialchars($item['file'], ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?></a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<?php if ($is_admin && $default_admin_user === 'admin'): ?>
<div class="flash">
<strong style="color: var(--primary);">Sécurité critique :</strong> Les identifiants par défaut sont actifs.
(<code><?php echo htmlspecialchars($default_admin_user, ENT_QUOTES, 'UTF-8'); ?></code> / <code><?php echo htmlspecialchars($default_admin_password, ENT_QUOTES, 'UTF-8'); ?></code>)
<br><small>Veuillez modifier ces accès dès maintenant.</small>
</div>
<?php endif; ?>
<?php if ($is_admin): ?>
<main class="admin-content">
<!-- Form Card -->
<section class="glass-card">
<h2><?php echo $edit_cl_auth_id > 0 ? 'Mise à jour sujet' : 'Nouveau sujet'; ?></h2>
<form method="post" action="admin.php<?php echo $edit_cl_auth_id > 0 ? '?edit=' . $edit_cl_auth_id : ''; ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="admin_action" value="<?php echo $edit_cl_auth_id > 0 ? 'update' : 'create'; ?>">
<?php if ($edit_cl_auth_id > 0): ?>
<input type="hidden" name="cl_auth_id" value="<?php echo $edit_cl_auth_id; ?>">
<?php endif; ?>
<div class="form-group">
<label for="cl_auth_user">Identifiant Système</label>
<input class="form-control" id="cl_auth_user" name="cl_auth_user" type="text" required value="<?php echo htmlspecialchars($edit_cl_auth_user, ENT_QUOTES, 'UTF-8'); ?>" placeholder="Nom d'utilisateur">
</div>
<div class="form-group">
<label for="cl_auth_pass"><?php echo $edit_cl_auth_id > 0 ? 'Nouveau Secret (vide = inchangé)' : 'Secret d\'accès'; ?></label>
<input class="form-control" id="cl_auth_pass" name="cl_auth_pass" type="password" <?php echo $edit_cl_auth_id > 0 ? '' : 'required'; ?> placeholder="••••••••">
</div>
<div class="form-group">
<label for="cl_auth_right">Niveau d'accréditation</label>
<select class="form-control" id="cl_auth_right" name="cl_auth_right">
<option value="admin" <?php echo $edit_cl_auth_right === 'admin' ? 'selected' : ''; ?>>Administrateur</option>
<option value="moderator" <?php echo $edit_cl_auth_right === 'moderator' ? 'selected' : ''; ?>>Modérateur</option>
<option value="member" <?php echo $edit_cl_auth_right === 'member' ? 'selected' : ''; ?>>Membre</option>
</select>
</div>
<div style="display: flex; gap: 10px; margin-top: 2rem;">
<button class="btn-modern" style="flex: 2;" type="submit">
<?php echo $edit_cl_auth_id > 0 ? 'Appliquer' : 'Initialiser'; ?>
</button>
<?php if ($edit_cl_auth_id > 0): ?>
<a class="btn-modern danger" style="flex: 1;" href="admin.php">Annuler</a>
<?php endif; ?>
</div>
</form>
</section>
<!-- Table Card -->
<section class="glass-card">
<h2>Base de données sujets</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>UID</th>
<th>Sujet</th>
<th>Accréditation</th>
<th style="text-align: right;">Opérations</th>
</tr>
</thead>
<tbody>
<?php if (empty($tbl_auth_all)): ?>
<tr>
<td colspan="4" class="empty-state">Aucun sujet détecté dans la base.</td>
</tr>
<?php else: ?>
<?php foreach ($tbl_auth_all as $tbl_auth): ?>
<?php
$cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$cl_auth_right = (string) $tbl_auth['cl_auth_right'];
?>
<tr>
<td><span class="user-id">#<?php echo sprintf('%03d', $cl_auth_id); ?></span></td>
<td><strong><?php echo htmlspecialchars($cl_auth_user, ENT_QUOTES, 'UTF-8'); ?></strong></td>
<td>
<span class="badge <?php echo $cl_auth_right === 'admin' ? 'badge-admin' : ($cl_auth_right === 'moderator' ? 'badge-moderator' : 'badge-member'); ?>">
<?php echo htmlspecialchars(auth_role_label($cl_auth_right), ENT_QUOTES, 'UTF-8'); ?>
</span>
</td>
<td>
<div class="row-actions" style="justify-content: flex-end;">
<a class="btn-modern btn-mini" href="admin.php?edit=<?php echo $cl_auth_id; ?>">Editer</a>
<form method="post" action="admin.php" onsubmit="return confirm('Terminer ce sujet ? Cette action est irréversible.');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="admin_action" value="delete">
<input type="hidden" name="cl_auth_id" value="<?php echo $cl_auth_id; ?>">
<button class="btn-modern btn-mini danger" type="submit">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
<?php else: ?>
<main class="admin-content" style="grid-template-columns: 1fr;">
<section class="glass-card">
<h2>Zone <?php echo htmlspecialchars($current_role_label, ENT_QUOTES, 'UTF-8'); ?></h2>
<p>Vous êtes bien entré dans la zone admin avec un compte <strong><?php echo htmlspecialchars($current_role_label, ENT_QUOTES, 'UTF-8'); ?></strong>.</p>
<p>La gestion des utilisateurs reste réservée aux administrateurs, mais vous pouvez utiliser ci-dessous les pages ouvertes à votre niveau d'autorisation.</p>
<?php if (empty($user_accessible_items)): ?>
<div class="empty-state">Aucune page ne vous a encore été attribuée pour ce rôle.</div>
<?php else: ?>
<div class="row-actions" style="flex-wrap: wrap; gap: 12px; margin-top: 1rem;">
<?php foreach ($user_accessible_items as $item): ?>
<a class="btn-modern" href="<?php echo htmlspecialchars($item['file'], ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
</main>
<?php endif; ?>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

View File

@ -26,12 +26,10 @@ perspective effects (not including the modals and the overlay).
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: min(92vw, 1100px); width: 50%;
max-width: calc(100vw - 32px); max-width: 1400px;
min-width: 320px; min-width: 1200px;
max-height: calc(100vh - 32px);
height: auto; height: auto;
overflow-y: auto;
z-index: 2000; z-index: 2000;
visibility: hidden; visibility: hidden;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
@ -74,13 +72,6 @@ perspective effects (not including the modals and the overlay).
position: relative; position: relative;
border-radius: 3px; border-radius: 3px;
margin: 0 auto; margin: 0 auto;
max-width: 100%;
overflow: hidden;
}
.md-content img {
max-width: 100%;
height: auto;
} }
.md-content h3 { .md-content h3 {
@ -158,79 +149,3 @@ perspective effects (not including the modals and the overlay).
50% { transform: translateZ(-250px) rotateY(89deg); opacity: 1; animation-timing-function: ease-in;} 50% { transform: translateZ(-250px) rotateY(89deg); opacity: 1; animation-timing-function: ease-in;}
100% { transform: translateZ(0) rotateY(0deg); opacity: 1; } 100% { transform: translateZ(0) rotateY(0deg); opacity: 1; }
} }
@media (max-width: 1280px) {
.md-modal {
width: min(94vw, 960px);
min-width: 0;
}
}
@media (max-width: 900px) {
.md-content > div img.float-left,
.md-content > div img.float-right {
float: none;
display: block;
margin: 0 auto 16px;
max-width: min(100%, 260px);
}
}
@media (max-width: 768px) {
.md-modal {
width: calc(100vw - 24px);
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
}
.md-content h3 {
position: relative;
padding: 0.65em 3.4rem;
font-size: 1.5em;
line-height: 1.3;
}
.md-content > div {
padding: 16px 18px 20px;
font-size: 1em;
}
.md-content h3 img.float-left {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
float: none;
}
.md-content h3 .frame-icon-close.float-right {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
float: none;
}
}
@media (max-width: 480px) {
.md-modal {
width: calc(100vw - 16px);
max-width: calc(100vw - 16px);
min-width: 0;
max-height: calc(100vh - 16px);
}
.md-content h3 {
padding: 0.85em 2.8rem;
font-size: 1.15em;
}
.md-content > div {
padding: 14px 12px 18px;
font-size: 0.95em;
}
.md-content > div p {
text-align: left;
}
}

View File

@ -1,4 +1,3 @@
[hidden] { display: none !important; }
@font-face { @font-face {
font-family: 'Electrolize'; font-family: 'Electrolize';
src: url('../fonts/Electrolize-Regular.ttf') format('truetype'); src: url('../fonts/Electrolize-Regular.ttf') format('truetype');
@ -170,7 +169,6 @@ a:hover {
border: solid 3px rgb(155 145 60 / 25%); border: solid 3px rgb(155 145 60 / 25%);
border-radius: 10px; border-radius: 10px;
padding: 5px 0px 5px 0px; padding: 5px 0px 5px 0px;
text-align: center;
} }
.connexion-div-menu { .connexion-div-menu {
@ -190,7 +188,6 @@ a:hover {
justify-content: center; justify-content: center;
border: solid 3px rgb(155 145 60 / 25%); border: solid 3px rgb(155 145 60 / 25%);
border-radius: 10px; border-radius: 10px;
text-align: center;
} }
.center-div-menu { .center-div-menu {
@ -313,224 +310,3 @@ a:hover {
background-color: rgb(0 0 0 / 20%); background-color: rgb(0 0 0 / 20%);
cursor: pointer; cursor: pointer;
} }
/* Auth / admin helpers */
.connexion-div-menu {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.connexion-div-menu #accountLabel {
display: inline-block;
}
.connexion-div-menu.is-authenticated {
cursor: default;
height: auto;
min-height: 78px;
padding: 10px 14px;
flex-direction: column;
justify-content: center;
gap: 10px;
}
.connexion-div-menu.is-authenticated #accountLabel {
font-size: 18px;
font-weight: 400;
line-height: 1.2;
color: #f7edcf;
}
.connexion-div-menu.md-trigger,
.connexion-div-menu.md-trigger #accountLabel {
cursor: pointer;
user-select: none;
}
.connexion-actions[hidden] { display: none !important; }
.connexion-actions {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.connexion-action-btn {
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #f4e3b2;
text-decoration: none;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(244, 227, 178, 0.35);
border-radius: 10px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.connexion-action-btn:hover {
color: #ffffff;
background: rgba(155, 145, 60, 0.28);
border-color: rgba(255, 255, 255, 0.55);
transform: translateY(-1px);
}
.connexion-action-btn:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.75);
outline-offset: 2px;
}
.connexion-action-icon {
display: inline-flex;
width: 20px;
height: 20px;
}
.connexion-action-icon svg {
width: 100%;
height: 100%;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.login-status {
min-height: 18px;
}
.login-status.is-error {
color: #ff8080;
}
.login-status.is-success {
color: #9fe29f;
}
.assets-div-menu a,
.connexion-div-menu a,
.connexion-div-menu #accountLabel {
max-width: 100%;
word-break: break-word;
}
@media (max-width: 1200px) {
.center-page-bops {
width: min(48vw, 480px);
height: min(48vw, 480px);
top: 32%;
}
.center-div-menu {
width: min(1050px, calc(100vw - 40px));
}
}
@media (max-width: 900px) {
.assets-div-menu,
.connexion-div-menu {
width: auto;
max-width: none;
left: 16px;
right: 16px;
}
.assets-div-menu {
top: 16px;
}
.connexion-div-menu {
top: auto;
bottom: 16px;
justify-content: center;
min-height: 44px;
}
.center-page-bops {
width: min(62vw, 340px);
height: min(62vw, 340px);
top: 28%;
}
.center-div-menu {
width: calc(100vw - 32px);
top: 56%;
}
.center-div-menu .padding50 {
padding: 24px !important;
}
}
@media (max-width: 640px) {
.assets-div-menu {
gap: 6px;
padding: 10px 12px;
}
.connexion-div-menu {
gap: 8px;
padding: 8px 12px;
}
.connexion-div-menu.is-authenticated {
min-height: 84px;
padding: 10px 12px;
}
.connexion-actions {
flex-wrap: wrap;
justify-content: center;
}
.center-page-bops {
top: 170px;
width: min(68vw, 280px);
height: min(68vw, 280px);
}
.center-div-menu {
position: relative;
top: auto;
left: auto;
transform: none;
margin: 230px auto 100px;
width: calc(100vw - 24px);
}
.center-div-menu .padding50 {
padding: 18px !important;
}
.txt-s40 {
font-size: clamp(28px, 7vw, 40px);
}
.txt-s22 {
font-size: clamp(18px, 4.6vw, 22px);
}
}
@media (max-width: 480px) {
.assets-div-menu,
.connexion-div-menu {
left: 12px;
right: 12px;
}
.center-div-menu {
width: calc(100vw - 16px);
margin-top: 210px;
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,402 +0,0 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: 127.0.0.1 Database: app_39514
-- ------------------------------------------------------
-- Server version 10.11.14-MariaDB-0+deb12u2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `tbl_auth`
--
DROP TABLE IF EXISTS `tbl_auth`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_auth` (
`cl_auth_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_auth_user` varchar(190) NOT NULL,
`cl_auth_pass` varchar(255) NOT NULL,
`cl_auth_right` enum('admin','moderator','member') NOT NULL DEFAULT 'member',
PRIMARY KEY (`cl_auth_id`),
UNIQUE KEY `cl_auth_user` (`cl_auth_user`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_page_access`
--
DROP TABLE IF EXISTS `tbl_page_access`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_page_access` (
`cl_page_access_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_page_key` varchar(190) NOT NULL,
`cl_page_file` varchar(190) NOT NULL,
`cl_page_label` varchar(190) NOT NULL,
`cl_allow_admin` tinyint(1) NOT NULL DEFAULT 1,
`cl_allow_moderator` tinyint(1) NOT NULL DEFAULT 0,
`cl_allow_member` tinyint(1) NOT NULL DEFAULT 0,
`cl_updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_page_access_id`),
UNIQUE KEY `cl_page_key` (`cl_page_key`),
UNIQUE KEY `cl_page_file` (`cl_page_file`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scbanners`
--
DROP TABLE IF EXISTS `tbl_scbanners`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scbanners` (
`cl_scbanner_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scbanner_name` varchar(255) NOT NULL,
`cl_scbanner_url` text NOT NULL,
`cl_scbanner_border_color` varchar(20) NOT NULL DEFAULT '#ffae00',
PRIMARY KEY (`cl_scbanner_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_sccharacteritems`
--
DROP TABLE IF EXISTS `tbl_sccharacteritems`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_sccharacteritems` (
`cl_sccharacteritem_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_sccharacteritem_character_id` int(10) unsigned NOT NULL,
`cl_sccharacteritem_source` enum('base','custom') NOT NULL DEFAULT 'base',
`cl_sccharacteritem_scobjs_id` int(10) unsigned DEFAULT NULL,
`cl_sccharacteritem_scitemcustom_id` int(11) DEFAULT NULL,
`cl_sccharacteritem_slot` varchar(120) NOT NULL DEFAULT '',
`cl_sccharacteritem_quantity` int(10) unsigned DEFAULT NULL,
`cl_sccharacteritem_note` text DEFAULT NULL,
`cl_sccharacteritem_sort_order` int(10) unsigned NOT NULL DEFAULT 0,
`cl_sccharacteritem_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_sccharacteritem_id`),
KEY `idx_sccharacteritem_character` (`cl_sccharacteritem_character_id`),
KEY `idx_sccharacteritem_scobjs` (`cl_sccharacteritem_scobjs_id`),
KEY `idx_sccharacteritem_scitemcustom` (`cl_sccharacteritem_scitemcustom_id`),
KEY `idx_sccharacteritem_character_sort` (`cl_sccharacteritem_character_id`,`cl_sccharacteritem_sort_order`,`cl_sccharacteritem_id`),
CONSTRAINT `fk_sccharacteritem_character` FOREIGN KEY (`cl_sccharacteritem_character_id`) REFERENCES `tbl_sccharacters` (`cl_sccharacter_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_sccharacteritem_scitemcustom` FOREIGN KEY (`cl_sccharacteritem_scitemcustom_id`) REFERENCES `tbl_scitemcustom` (`cl_scitemcustom_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_sccharacteritem_scobjs` FOREIGN KEY (`cl_sccharacteritem_scobjs_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_sccharacters`
--
DROP TABLE IF EXISTS `tbl_sccharacters`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_sccharacters` (
`cl_sccharacter_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_sccharacter_owner_auth_id` int(10) unsigned NOT NULL,
`cl_sccharacter_name` varchar(190) NOT NULL,
`cl_sccharacter_role` varchar(190) NOT NULL DEFAULT '',
`cl_sccharacter_faction` varchar(190) NOT NULL DEFAULT '',
`cl_sccharacter_org_rsi_url` varchar(255) NOT NULL DEFAULT '',
`cl_sccharacter_is_player` tinyint(1) NOT NULL DEFAULT 0,
`cl_sccharacter_player_handle` varchar(190) NOT NULL DEFAULT '',
`cl_sccharacter_avatar_url` varchar(255) NOT NULL DEFAULT '',
`cl_sccharacter_description` text DEFAULT NULL,
`cl_sccharacter_notes` text DEFAULT NULL,
`cl_sccharacter_share_token` varchar(64) NOT NULL,
`cl_sccharacter_share_enabled` tinyint(1) NOT NULL DEFAULT 0,
`cl_sccharacter_is_pinned` tinyint(1) NOT NULL DEFAULT 0,
`cl_sccharacter_category_order` text DEFAULT NULL,
`cl_sccharacter_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`cl_sccharacter_updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_sccharacter_id`),
UNIQUE KEY `uq_sccharacter_share_token` (`cl_sccharacter_share_token`),
KEY `idx_sccharacter_owner` (`cl_sccharacter_owner_auth_id`),
KEY `idx_sccharacter_name` (`cl_sccharacter_name`),
CONSTRAINT `fk_sccharacter_owner_auth` FOREIGN KEY (`cl_sccharacter_owner_auth_id`) REFERENCES `tbl_auth` (`cl_auth_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scitemcustom`
--
DROP TABLE IF EXISTS `tbl_scitemcustom`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scitemcustom` (
`cl_scitemcustom_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scitemcustom_owner_auth_id` int(10) unsigned DEFAULT NULL,
`cl_scitemcustom_obj_id` int(10) unsigned NOT NULL,
`cl_scitemcustom_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scitemcustom_id`),
KEY `idx_scitemcustom_obj` (`cl_scitemcustom_obj_id`),
KEY `idx_scitemcustom_owner` (`cl_scitemcustom_owner_auth_id`),
CONSTRAINT `fk_scitemcustom_obj` FOREIGN KEY (`cl_scitemcustom_obj_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_scitemcustom_owner_auth` FOREIGN KEY (`cl_scitemcustom_owner_auth_id`) REFERENCES `tbl_auth` (`cl_auth_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scitemcustomstat`
--
DROP TABLE IF EXISTS `tbl_scitemcustomstat`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scitemcustomstat` (
`cl_scitemcustomstat_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scitemcustomstat_itemcustom_id` int(11) NOT NULL,
`cl_scitemcustomstat_stat_id` int(11) NOT NULL,
`cl_scitemcustomstat_sign` enum('+','','-') NOT NULL DEFAULT '+',
`cl_scitemcustomstat_value` decimal(10,2) NOT NULL DEFAULT 0.00,
`cl_scitemcustomstat_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scitemcustomstat_id`),
UNIQUE KEY `uq_scitemcustomstat_item_stat` (`cl_scitemcustomstat_itemcustom_id`,`cl_scitemcustomstat_stat_id`),
KEY `idx_scitemcustomstat_item` (`cl_scitemcustomstat_itemcustom_id`),
KEY `idx_scitemcustomstat_stat` (`cl_scitemcustomstat_stat_id`),
CONSTRAINT `fk_scitemcustomstat_item` FOREIGN KEY (`cl_scitemcustomstat_itemcustom_id`) REFERENCES `tbl_scitemcustom` (`cl_scitemcustom_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_scitemcustomstat_stat` FOREIGN KEY (`cl_scitemcustomstat_stat_id`) REFERENCES `tbl_scstatsitem` (`cl_scstatsitem_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmanufactures`
--
DROP TABLE IF EXISTS `tbl_scmanufactures`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmanufactures` (
`cl_scmanufactures_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmanufactures_name` varchar(255) NOT NULL,
PRIMARY KEY (`cl_scmanufactures_id`),
UNIQUE KEY `cl_scmanufactures_name` (`cl_scmanufactures_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmanutentionitems`
--
DROP TABLE IF EXISTS `tbl_scmanutentionitems`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmanutentionitems` (
`cl_scmanutentionitem_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmanutentionitem_manutention_id` int(10) unsigned NOT NULL,
`cl_scmanutentionitem_source` enum('base','custom') NOT NULL DEFAULT 'base',
`cl_scmanutentionitem_scobjs_id` int(10) unsigned DEFAULT NULL,
`cl_scmanutentionitem_scitemcustom_id` int(11) DEFAULT NULL,
`cl_scmanutentionitem_quantity` int(10) unsigned NOT NULL DEFAULT 1,
`cl_scmanutentionitem_extra_info` text DEFAULT NULL,
`cl_scmanutentionitem_sort_order` int(10) unsigned NOT NULL DEFAULT 0,
`cl_scmanutentionitem_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`cl_scmanutentionitem_updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_scmanutentionitem_id`),
KEY `idx_scmanutentionitem_sheet` (`cl_scmanutentionitem_manutention_id`),
KEY `idx_scmanutentionitem_scobjs` (`cl_scmanutentionitem_scobjs_id`),
KEY `idx_scmanutentionitem_scitemcustom` (`cl_scmanutentionitem_scitemcustom_id`),
KEY `idx_scmanutentionitem_sheet_sort` (`cl_scmanutentionitem_manutention_id`,`cl_scmanutentionitem_sort_order`,`cl_scmanutentionitem_id`),
CONSTRAINT `fk_scmanutentionitem_scitemcustom` FOREIGN KEY (`cl_scmanutentionitem_scitemcustom_id`) REFERENCES `tbl_scitemcustom` (`cl_scitemcustom_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_scmanutentionitem_scobjs` FOREIGN KEY (`cl_scmanutentionitem_scobjs_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_scmanutentionitem_sheet` FOREIGN KEY (`cl_scmanutentionitem_manutention_id`) REFERENCES `tbl_scmanutentions` (`cl_scmanutention_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmanutentions`
--
DROP TABLE IF EXISTS `tbl_scmanutentions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmanutentions` (
`cl_scmanutention_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmanutention_owner_auth_id` int(10) unsigned NOT NULL,
`cl_scmanutention_title` varchar(190) NOT NULL,
`cl_scmanutention_type` varchar(120) NOT NULL DEFAULT '',
`cl_scmanutention_subtype` varchar(120) NOT NULL DEFAULT '',
`cl_scmanutention_description` text DEFAULT NULL,
`cl_scmanutention_share_token` varchar(64) NOT NULL,
`cl_scmanutention_share_enabled` tinyint(1) NOT NULL DEFAULT 0,
`cl_scmanutention_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`cl_scmanutention_updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`cl_scmanutention_id`),
UNIQUE KEY `uq_scmanutention_share_token` (`cl_scmanutention_share_token`),
KEY `idx_scmanutention_owner` (`cl_scmanutention_owner_auth_id`),
KEY `idx_scmanutention_title` (`cl_scmanutention_title`),
CONSTRAINT `fk_scmanutention_owner_auth` FOREIGN KEY (`cl_scmanutention_owner_auth_id`) REFERENCES `tbl_auth` (`cl_auth_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scmining`
--
DROP TABLE IF EXISTS `tbl_scmining`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scmining` (
`cl_scmining_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scmining_obj_id` int(10) unsigned NOT NULL,
`cl_scmining_scan_value` int(10) unsigned DEFAULT 0,
`cl_scmining_max_occurrence` int(10) unsigned DEFAULT 1,
`cl_scmining_can_manual` tinyint(1) DEFAULT 0,
`cl_scmining_can_land` tinyint(1) DEFAULT 0,
`cl_scmining_can_space` tinyint(1) DEFAULT 0,
PRIMARY KEY (`cl_scmining_id`),
UNIQUE KEY `cl_scmining_obj_id` (`cl_scmining_obj_id`),
CONSTRAINT `fk_scmining_obj` FOREIGN KEY (`cl_scmining_obj_id`) REFERENCES `tbl_scobjs` (`cl_scobjs_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scnotifications`
--
DROP TABLE IF EXISTS `tbl_scnotifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scnotifications` (
`cl_scnotification_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scnotification_webhook_id` int(11) NOT NULL,
`cl_scnotification_banner_id` int(11) DEFAULT NULL,
`cl_scnotification_title` varchar(255) NOT NULL DEFAULT '',
`cl_scnotification_message` text NOT NULL,
`cl_scnotification_payload` longtext NOT NULL,
`cl_scnotification_response` longtext DEFAULT NULL,
`cl_scnotification_success` tinyint(1) NOT NULL DEFAULT 0,
`cl_scnotification_created_by` varchar(190) NOT NULL,
`cl_scnotification_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scnotification_id`),
KEY `idx_scnotification_webhook` (`cl_scnotification_webhook_id`),
KEY `idx_scnotification_banner` (`cl_scnotification_banner_id`),
CONSTRAINT `fk_scnotification_banner` FOREIGN KEY (`cl_scnotification_banner_id`) REFERENCES `tbl_scbanners` (`cl_scbanner_id`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `fk_scnotification_webhook` FOREIGN KEY (`cl_scnotification_webhook_id`) REFERENCES `tbl_scwebhooks` (`cl_scwebhook_id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scobjs`
--
DROP TABLE IF EXISTS `tbl_scobjs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scobjs` (
`cl_scobjs_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scobjs_name` varchar(255) NOT NULL,
`cl_scobjs_type` varchar(100) DEFAULT NULL,
`cl_scobjs_subtype` varchar(100) DEFAULT NULL,
`cl_scobjs_uuid` varchar(100) NOT NULL,
`cl_scobjs_rarity` varchar(10) DEFAULT '',
`cl_scobjs_about` text DEFAULT NULL,
`cl_scobjs_description` text DEFAULT NULL,
PRIMARY KEY (`cl_scobjs_id`),
UNIQUE KEY `cl_scobjs_uuid` (`cl_scobjs_uuid`)
) ENGINE=InnoDB AUTO_INCREMENT=18306 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scpreset`
--
DROP TABLE IF EXISTS `tbl_scpreset`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scpreset` (
`cl_scpreset_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scpreset_manufacture_id` int(10) unsigned DEFAULT NULL,
`cl_scpreset_vaisseau_id` int(10) unsigned DEFAULT NULL,
`cl_scpreset_name` varchar(255) NOT NULL,
`cl_scpreset_manufacturer` varchar(255) NOT NULL,
`cl_scpreset_description` text DEFAULT NULL,
`cl_scpreset_link` varchar(255) NOT NULL,
`cl_scpreset_creator` varchar(255) DEFAULT 'admin',
PRIMARY KEY (`cl_scpreset_id`),
KEY `fk_scpreset_manufacture` (`cl_scpreset_manufacture_id`),
KEY `fk_scpreset_vaisseau` (`cl_scpreset_vaisseau_id`),
CONSTRAINT `fk_scpreset_manufacture` FOREIGN KEY (`cl_scpreset_manufacture_id`) REFERENCES `tbl_scmanufactures` (`cl_scmanufactures_id`),
CONSTRAINT `fk_scpreset_vaisseau` FOREIGN KEY (`cl_scpreset_vaisseau_id`) REFERENCES `tbl_scvaisseaux` (`cl_scvaisseaux_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scstatsitem`
--
DROP TABLE IF EXISTS `tbl_scstatsitem`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scstatsitem` (
`cl_scstatsitem_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scstatsitem_name` varchar(255) NOT NULL,
`cl_scstatsitem_unit` varchar(10) NOT NULL DEFAULT '%',
`cl_scstatsitem_created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`cl_scstatsitem_id`),
UNIQUE KEY `uq_scstatsitem_name` (`cl_scstatsitem_name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scvaisseaux`
--
DROP TABLE IF EXISTS `tbl_scvaisseaux`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scvaisseaux` (
`cl_scvaisseaux_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cl_scvaisseaux_name` varchar(255) NOT NULL,
`cl_scvaisseaux_manufacture_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`cl_scvaisseaux_id`),
KEY `fk_vaisseaux_manufacture` (`cl_scvaisseaux_manufacture_id`),
CONSTRAINT `fk_vaisseaux_manufacture` FOREIGN KEY (`cl_scvaisseaux_manufacture_id`) REFERENCES `tbl_scmanufactures` (`cl_scmanufactures_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tbl_scwebhooks`
--
DROP TABLE IF EXISTS `tbl_scwebhooks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tbl_scwebhooks` (
`cl_scwebhook_id` int(11) NOT NULL AUTO_INCREMENT,
`cl_scwebhook_name` varchar(255) NOT NULL,
`cl_scwebhook_url` text NOT NULL,
`cl_scwebhook_image_url` text NOT NULL,
`cl_scwebhook_border_color` varchar(20) NOT NULL DEFAULT '#ffae00',
`cl_scwebhook_is_forum` tinyint(1) DEFAULT 0,
PRIMARY KEY (`cl_scwebhook_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-05-07 0:10:56

View File

@ -1,510 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function auth_config_value(array $environment_keys, array $constant_keys = []): ?string
{
foreach ($environment_keys as $environment_key) {
$value = getenv($environment_key);
if ($value !== false) {
$value = trim((string) $value);
if ($value !== '') {
return $value;
}
}
}
foreach ($constant_keys as $constant_key) {
if (defined($constant_key)) {
$value = trim((string) constant($constant_key));
if ($value !== '') {
return $value;
}
}
}
return null;
}
function auth_start_session(): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
}
function auth_bootstrap(): void
{
static $auth_bootstrap_done = false;
if ($auth_bootstrap_done) {
return;
}
$pdo = db();
$pdo->exec(
"CREATE TABLE IF NOT EXISTS tbl_auth (
cl_auth_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cl_auth_user VARCHAR(190) NOT NULL UNIQUE,
cl_auth_pass VARCHAR(255) NOT NULL,
cl_auth_right ENUM('admin', 'moderator', 'member') NOT NULL DEFAULT 'member'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS tbl_page_access (
cl_page_access_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cl_page_key VARCHAR(190) NOT NULL UNIQUE,
cl_page_file VARCHAR(190) NOT NULL UNIQUE,
cl_page_label VARCHAR(190) NOT NULL,
cl_allow_admin TINYINT(1) NOT NULL DEFAULT 1,
cl_allow_moderator TINYINT(1) NOT NULL DEFAULT 0,
cl_allow_member TINYINT(1) NOT NULL DEFAULT 0,
cl_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$stmt_auth_role_column = $pdo->query("SHOW COLUMNS FROM tbl_auth LIKE 'cl_auth_right'");
$auth_role_column = $stmt_auth_role_column->fetch();
$auth_role_type = strtolower((string) ($auth_role_column['Type'] ?? ''));
if (strpos($auth_role_type, "'moderator'") === false) {
$pdo->exec("ALTER TABLE tbl_auth MODIFY cl_auth_right ENUM('admin', 'moderator', 'member') NOT NULL DEFAULT 'member'");
}
$stmt_page_access_columns = $pdo->query('SHOW COLUMNS FROM tbl_page_access');
$page_access_columns = [];
foreach ($stmt_page_access_columns->fetchAll() as $page_access_column) {
$page_access_columns[] = (string) ($page_access_column['Field'] ?? '');
}
if (!in_array('cl_allow_moderator', $page_access_columns, true)) {
$pdo->exec('ALTER TABLE tbl_page_access ADD COLUMN cl_allow_moderator TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_allow_admin');
$pdo->exec('UPDATE tbl_page_access SET cl_allow_moderator = cl_allow_member');
}
$sql_count_admin = "SELECT COUNT(*) FROM tbl_auth WHERE cl_auth_right = 'admin'";
$stmt_count_admin = $pdo->query($sql_count_admin);
$cl_auth_admin_total = (int) $stmt_count_admin->fetchColumn();
if ($cl_auth_admin_total === 0) {
[$cl_auth_user, $plain_default_password] = auth_default_admin_credentials();
if ($cl_auth_user === '' || $plain_default_password === '') {
throw new RuntimeException(
"Aucun administrateur n'existe et aucun couple DEFAULT_ADMIN_USER / DEFAULT_ADMIN_PASSWORD n'est configuré."
);
}
$cl_auth_pass = password_hash($plain_default_password, PASSWORD_DEFAULT);
$cl_auth_right = 'admin';
$stmt_insert_admin = $pdo->prepare(
'INSERT INTO tbl_auth (cl_auth_user, cl_auth_pass, cl_auth_right) VALUES (:cl_auth_user, :cl_auth_pass, :cl_auth_right)'
);
$stmt_insert_admin->execute([
'cl_auth_user' => $cl_auth_user,
'cl_auth_pass' => $cl_auth_pass,
'cl_auth_right' => $cl_auth_right,
]);
}
$auth_bootstrap_done = true;
}
function auth_default_admin_credentials(): array
{
$cl_auth_user = auth_config_value(
['DEFAULT_ADMIN_USER', 'APP_DEFAULT_ADMIN_USER'],
['DEFAULT_ADMIN_USER', 'APP_DEFAULT_ADMIN_USER']
) ?? 'admin';
$plain_default_password = auth_config_value(
['DEFAULT_ADMIN_PASSWORD', 'APP_DEFAULT_ADMIN_PASSWORD'],
['DEFAULT_ADMIN_PASSWORD', 'APP_DEFAULT_ADMIN_PASSWORD']
);
if ($plain_default_password === null) {
return ['', ''];
}
return [$cl_auth_user, $plain_default_password];
}
function auth_csrf_token(): string
{
auth_start_session();
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function auth_validate_csrf(?string $csrf_token): bool
{
auth_start_session();
if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
return false;
}
if ($csrf_token === null) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $csrf_token);
}
function auth_is_logged_in(): bool
{
auth_start_session();
return isset($_SESSION['user']) && isset($_SESSION['role']);
}
function auth_is_admin(): bool
{
return auth_current_role() === 'admin';
}
function auth_is_moderator(): bool
{
return auth_current_role() === 'moderator';
}
function auth_valid_roles(): array
{
return ['admin', 'moderator', 'member'];
}
function auth_role_label(string $role): string
{
static $labels = [
'admin' => 'Administrateur',
'moderator' => 'Modérateur',
'member' => 'Membre',
];
return $labels[$role] ?? ucfirst($role);
}
function auth_current_user(): string
{
auth_start_session();
return isset($_SESSION['user']) ? (string) $_SESSION['user'] : '';
}
function auth_current_role(): string
{
auth_start_session();
return isset($_SESSION['role']) ? (string) $_SESSION['role'] : '';
}
function auth_flash_set(string $flash_type, string $flash_message): void
{
auth_start_session();
$_SESSION['flash'] = [
'type' => $flash_type,
'message' => $flash_message,
];
}
function auth_flash_get(): ?array
{
auth_start_session();
if (!isset($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
return null;
}
$flash = $_SESSION['flash'];
unset($_SESSION['flash']);
return $flash;
}
function auth_page_basename(string $page_file): string
{
$basename = basename(trim($page_file));
if ($basename === '' || preg_match('/^[a-zA-Z0-9._-]+$/', $basename) !== 1) {
throw new InvalidArgumentException('Nom de page invalide.');
}
return $basename;
}
function auth_page_default_member_access(string $page_file): int
{
static $member_defaults = [
'scnotification.php' => 1,
'scpreset.php' => 1,
];
$page_file = auth_page_basename($page_file);
return $member_defaults[$page_file] ?? 0;
}
function auth_page_default_moderator_access(string $page_file): int
{
return auth_page_default_member_access($page_file);
}
function auth_page_access_defaults(string $page_file, string $page_label = ''): array
{
$normalized_page_file = auth_page_basename($page_file);
$normalized_page_label = trim($page_label) !== '' ? trim($page_label) : $normalized_page_file;
return [
'cl_page_key' => pathinfo($normalized_page_file, PATHINFO_FILENAME),
'cl_page_file' => $normalized_page_file,
'cl_page_label' => $normalized_page_label,
'cl_allow_admin' => 1,
'cl_allow_moderator' => auth_page_default_moderator_access($normalized_page_file),
'cl_allow_member' => auth_page_default_member_access($normalized_page_file),
];
}
function auth_page_access_ensure(string $page_file, string $page_label = ''): array
{
auth_bootstrap();
$defaults = auth_page_access_defaults($page_file, $page_label);
$pdo = db();
$stmt = $pdo->prepare(
'SELECT cl_page_access_id, cl_page_key, cl_page_file, cl_page_label, cl_allow_admin, cl_allow_moderator, cl_allow_member
FROM tbl_page_access
WHERE cl_page_file = :cl_page_file
LIMIT 1'
);
$stmt->execute([
'cl_page_file' => $defaults['cl_page_file'],
]);
$row = $stmt->fetch();
if (!$row) {
$stmt_insert = $pdo->prepare(
'INSERT INTO tbl_page_access (cl_page_key, cl_page_file, cl_page_label, cl_allow_admin, cl_allow_moderator, cl_allow_member)
VALUES (:cl_page_key, :cl_page_file, :cl_page_label, :cl_allow_admin, :cl_allow_moderator, :cl_allow_member)'
);
$stmt_insert->execute($defaults);
$stmt->execute([
'cl_page_file' => $defaults['cl_page_file'],
]);
$row = $stmt->fetch();
} elseif ($defaults['cl_page_label'] !== '' && (string) $row['cl_page_label'] !== $defaults['cl_page_label']) {
$stmt_update_label = $pdo->prepare(
'UPDATE tbl_page_access SET cl_page_label = :cl_page_label WHERE cl_page_file = :cl_page_file'
);
$stmt_update_label->execute([
'cl_page_label' => $defaults['cl_page_label'],
'cl_page_file' => $defaults['cl_page_file'],
]);
$row['cl_page_label'] = $defaults['cl_page_label'];
}
if (!$row) {
throw new RuntimeException('Impossible d\'initialiser la configuration d\'accès de la page.');
}
$row['cl_allow_admin'] = (int) ($row['cl_allow_admin'] ?? 1);
$row['cl_allow_moderator'] = (int) ($row['cl_allow_moderator'] ?? 0);
$row['cl_allow_member'] = (int) ($row['cl_allow_member'] ?? 0);
return $row;
}
function auth_user_can_access_page(string $page_file, string $page_label = ''): bool
{
auth_start_session();
auth_bootstrap();
if (!auth_is_logged_in()) {
return false;
}
$role = auth_current_role();
if ($role === 'admin') {
return true;
}
$row = auth_page_access_ensure($page_file, $page_label);
if ($role === 'moderator') {
return (int) $row['cl_allow_moderator'] === 1;
}
if ($role === 'member') {
return (int) $row['cl_allow_member'] === 1;
}
return false;
}
function auth_require_page_access(string $page_file, string $page_label = ''): void
{
if (!auth_is_logged_in()) {
header('Location: index.php');
exit;
}
if (auth_is_admin()) {
auth_page_access_ensure($page_file, $page_label);
return;
}
if (auth_user_can_access_page($page_file, $page_label)) {
return;
}
auth_flash_set('error', 'Accès refusé : cette page n\'est pas ouverte pour votre niveau d\'autorisation.');
header('Location: index.php');
exit;
}
function auth_handle_page_access_post(string $page_file, string $page_label = ''): void
{
auth_start_session();
auth_bootstrap();
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
return;
}
if (!isset($_POST['page_access_action'])) {
return;
}
$redirect_target = auth_page_basename($page_file);
if (!auth_is_admin()) {
auth_flash_set('error', 'Seul un administrateur peut modifier les accès de page.');
header('Location: index.php');
exit;
}
$csrf_token = isset($_POST['csrf_token']) ? (string) $_POST['csrf_token'] : null;
if (!auth_validate_csrf($csrf_token)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: ' . $redirect_target);
exit;
}
$row = auth_page_access_ensure($page_file, $page_label);
$cl_allow_moderator = isset($_POST['cl_allow_moderator']) ? 1 : 0;
$cl_allow_member = isset($_POST['cl_allow_member']) ? 1 : 0;
$stmt = db()->prepare(
'UPDATE tbl_page_access
SET cl_page_label = :cl_page_label,
cl_allow_admin = 1,
cl_allow_moderator = :cl_allow_moderator,
cl_allow_member = :cl_allow_member
WHERE cl_page_file = :cl_page_file'
);
$stmt->execute([
'cl_page_label' => $row['cl_page_label'],
'cl_allow_moderator' => $cl_allow_moderator,
'cl_allow_member' => $cl_allow_member,
'cl_page_file' => $row['cl_page_file'],
]);
auth_flash_set('success', 'Accès mis à jour pour ' . $row['cl_page_label'] . '.');
header('Location: ' . $redirect_target);
exit;
}
function auth_render_page_access_widget(string $page_file, string $page_label = ''): string
{
if (!auth_is_admin()) {
return '';
}
$row = auth_page_access_ensure($page_file, $page_label);
$csrf_token = auth_csrf_token();
$action = htmlspecialchars($row['cl_page_file'], ENT_QUOTES, 'UTF-8');
$label = htmlspecialchars((string) $row['cl_page_label'], ENT_QUOTES, 'UTF-8');
$csrf = htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8');
$moderator_checked = (int) $row['cl_allow_moderator'] === 1 ? 'checked' : '';
$member_checked = (int) $row['cl_allow_member'] === 1 ? 'checked' : '';
return <<<HTML
<div style="position:fixed;top:10px;right:10px;z-index:9999;background:rgba(10,14,18,0.94);border:1px solid rgba(162,155,120,0.45);border-radius:12px;padding:10px 12px;box-shadow:0 10px 24px rgba(0,0,0,0.35);backdrop-filter:blur(8px);font-family:Arial,sans-serif;color:#f2f2f2;min-width:220px;max-width:min(92vw,280px);">
<form method="post" action="{$action}" style="margin:0;display:flex;flex-direction:column;gap:8px;">
<input type="hidden" name="csrf_token" value="{$csrf}">
<input type="hidden" name="page_access_action" value="save">
<div style="font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#a29b78;">Accès page</div>
<div style="font-size:14px;font-weight:700;line-height:1.25;">{$label}</div>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;opacity:.9;">
<input type="checkbox" checked disabled>
<span>Admin <small style="opacity:.7;">(toujours autorisé)</small></span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" name="cl_allow_moderator" value="1" {$moderator_checked}>
<span>Modérateur</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" name="cl_allow_member" value="1" {$member_checked}>
<span>Membre</span>
</label>
<button type="submit" style="appearance:none;border:0;border-radius:8px;padding:8px 10px;background:#a29b78;color:#111;font-weight:700;cursor:pointer;">Appliquer</button>
</form>
</div>
HTML;
}
function auth_navigation_items(): array
{
return [
['file' => 'admin.php', 'label' => 'Utilisateurs', 'admin_only' => true],
['file' => 'scwebhook.php', 'label' => 'WEBHOOK'],
['file' => 'scnotification.php', 'label' => 'NOTIF DISCORD'],
['file' => 'scitems.php', 'label' => 'Base d\'Objets'],
['file' => 'scstatsitem.php', 'label' => 'Stats Item'],
['file' => 'scitemcustom.php', 'label' => 'Objets perso.'],
['file' => 'sccharacters.php', 'label' => 'Personnages'],
['file' => 'scmanutention.php', 'label' => 'Manutention'],
['file' => 'scmining.php', 'label' => 'Scanner Minage'],
['file' => 'scmanufactures.php', 'label' => 'Manufactures'],
['file' => 'scvaisseaux.php', 'label' => 'Vaisseaux'],
['file' => 'scpreset.php', 'label' => 'Presets Vaisseau'],
];
}
function auth_render_app_nav(string $current_page): string
{
if (!auth_is_logged_in()) {
return '';
}
$current_page = auth_page_basename($current_page);
$html = '<nav class="nav-tabs">';
foreach (auth_navigation_items() as $item) {
$file = (string) $item['file'];
$label = (string) $item['label'];
$admin_only = !empty($item['admin_only']);
if ($admin_only && !auth_is_admin()) {
continue;
}
if (!auth_is_admin() && !auth_user_can_access_page($file, $label)) {
continue;
}
$is_active = $file === $current_page ? ' class="active"' : '';
$html .= '<a href="' . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '"' . $is_active . '>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</a>';
}
$html .= '</nav>';
return $html;
}

View File

@ -1,21 +0,0 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_39514');
define('DB_USER', 'app_39514');
define('DB_PASS', 'ee6da88c-09af-4b48-b728-7a55edfb4e42');
if (!defined('DISCORD_BOT_TOKEN')) {
define('DISCORD_BOT_TOKEN', 'MTQyNTgwNjIxOTMwNTY4MTAxOA.GGq1cp.CjHC7vQogGrX_HS2WnuXLf4XOLAnWLC5Cm-XA4');
}
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}

View File

@ -1,721 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function sccharacters_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function sccharacters_index_exists(PDO $db, string $table, string $index): bool
{
$stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
return (bool) $stmt->fetch();
}
function sccharacters_foreign_key_exists(PDO $db, string $table, string $constraint): bool
{
$stmt = $db->prepare(
"SELECT COUNT(*)
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND CONSTRAINT_NAME = :constraint_name
AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
);
$stmt->execute([
'table_name' => $table,
'constraint_name' => $constraint,
]);
return (int) $stmt->fetchColumn() > 0;
}
function sccharacters_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_sccharacters (
cl_sccharacter_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_sccharacter_owner_auth_id INT UNSIGNED NOT NULL,
cl_sccharacter_name VARCHAR(190) NOT NULL,
cl_sccharacter_role VARCHAR(190) NOT NULL DEFAULT '',
cl_sccharacter_faction VARCHAR(190) NOT NULL DEFAULT '',
cl_sccharacter_org_rsi_url VARCHAR(255) NOT NULL DEFAULT '',
cl_sccharacter_player_handle VARCHAR(190) NOT NULL DEFAULT '',
cl_sccharacter_avatar_url VARCHAR(255) NOT NULL DEFAULT '',
cl_sccharacter_description TEXT DEFAULT NULL,
cl_sccharacter_notes TEXT DEFAULT NULL,
cl_sccharacter_share_token VARCHAR(64) NOT NULL,
cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
cl_sccharacter_is_pinned TINYINT(1) NOT NULL DEFAULT 0,
cl_sccharacter_category_order TEXT DEFAULT NULL,
cl_sccharacter_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cl_sccharacter_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (cl_sccharacter_id),
UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token),
KEY idx_sccharacter_owner (cl_sccharacter_owner_auth_id),
KEY idx_sccharacter_name (cl_sccharacter_name),
CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_owner_auth_id')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_owner_auth_id INT UNSIGNED NULL AFTER cl_sccharacter_id'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_token')) {
$db->exec(
"ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_share_token VARCHAR(64) NOT NULL DEFAULT '' AFTER cl_sccharacter_notes"
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_enabled')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_token'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_is_pinned')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_is_pinned TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_enabled'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_org_rsi_url')) {
$db->exec(
"ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_org_rsi_url VARCHAR(255) NOT NULL DEFAULT '' AFTER cl_sccharacter_faction"
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_player_handle')) {
$db->exec(
"ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_player_handle VARCHAR(190) NOT NULL DEFAULT '' AFTER cl_sccharacter_org_rsi_url"
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_category_order')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD COLUMN cl_sccharacter_category_order TEXT NULL AFTER cl_sccharacter_is_pinned'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'idx_sccharacter_owner')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD INDEX idx_sccharacter_owner (cl_sccharacter_owner_auth_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'uq_sccharacter_share_token')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token)'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacters', 'fk_sccharacter_owner_auth')) {
$db->exec(
'ALTER TABLE tbl_sccharacters
ADD CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_sccharacteritems (
cl_sccharacteritem_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_sccharacteritem_character_id INT UNSIGNED NOT NULL,
cl_sccharacteritem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
cl_sccharacteritem_scobjs_id INT UNSIGNED DEFAULT NULL,
cl_sccharacteritem_scitemcustom_id INT(11) DEFAULT NULL,
cl_sccharacteritem_slot VARCHAR(120) NOT NULL DEFAULT '',
cl_sccharacteritem_quantity INT UNSIGNED DEFAULT NULL,
cl_sccharacteritem_note TEXT DEFAULT NULL,
cl_sccharacteritem_sort_order INT UNSIGNED NOT NULL DEFAULT 0,
cl_sccharacteritem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_sccharacteritem_id),
KEY idx_sccharacteritem_character (cl_sccharacteritem_character_id),
KEY idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id),
KEY idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id),
KEY idx_sccharacteritem_character_sort (cl_sccharacteritem_character_id, cl_sccharacteritem_sort_order, cl_sccharacteritem_id),
CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
REFERENCES tbl_sccharacters (cl_sccharacter_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE,
CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scobjs_id')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_scobjs_id INT UNSIGNED NULL AFTER cl_sccharacteritem_source'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scitemcustom_id')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_scitemcustom_id INT(11) NULL AFTER cl_sccharacteritem_scobjs_id'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_quantity')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_quantity INT UNSIGNED NULL AFTER cl_sccharacteritem_slot'
);
}
if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_sort_order')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD COLUMN cl_sccharacteritem_sort_order INT UNSIGNED NOT NULL DEFAULT 0 AFTER cl_sccharacteritem_note'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_character (cl_sccharacteritem_character_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id)'
);
}
if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character_sort')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD INDEX idx_sccharacteritem_character_sort (cl_sccharacteritem_character_id, cl_sccharacteritem_sort_order, cl_sccharacteritem_id)'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_character')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
REFERENCES tbl_sccharacters (cl_sccharacter_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_sccharacteritems
ADD CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
$stmt_missing_tokens = $db->query(
"SELECT cl_sccharacter_id
FROM tbl_sccharacters
WHERE cl_sccharacter_share_token IS NULL
OR cl_sccharacter_share_token = ''"
);
$stmt_update_token = $db->prepare(
'UPDATE tbl_sccharacters
SET cl_sccharacter_share_token = :token
WHERE cl_sccharacter_id = :id'
);
foreach ($stmt_missing_tokens->fetchAll() as $row) {
$stmt_update_token->execute([
'token' => sccharacters_generate_share_token($db),
'id' => (int) $row['cl_sccharacter_id'],
]);
}
$bootstrapped = true;
}
function sccharacters_generate_share_token(PDO $db): string
{
do {
$token = bin2hex(random_bytes(16));
$stmt = $db->prepare(
'SELECT cl_sccharacter_id
FROM tbl_sccharacters
WHERE cl_sccharacter_share_token = :token
LIMIT 1'
);
$stmt->execute(['token' => $token]);
} while ($stmt->fetch());
return $token;
}
function sccharacters_extract_org_tag(?string $url): string
{
$url = trim((string) $url);
if ($url === '') {
return '';
}
$path = trim((string) parse_url($url, PHP_URL_PATH));
if ($path === '') {
return '';
}
if (preg_match('~/(?:[a-z]{2}/)?orgs/([^/?#]+)~i', $path, $matches)) {
$tag = rawurldecode((string) ($matches[1] ?? ''));
} else {
$segments = array_values(array_filter(explode('/', trim($path, '/')), static fn (string $segment): bool => $segment !== ''));
$tag = $segments !== [] ? rawurldecode((string) end($segments)) : '';
}
$tag = preg_replace('/[^A-Za-z0-9._-]+/', '', (string) $tag);
if (!is_string($tag)) {
return '';
}
return strtoupper(trim($tag));
}
function sccharacters_resolve_org_tag(array $character): string
{
return sccharacters_extract_org_tag((string) ($character['cl_sccharacter_org_rsi_url'] ?? ''));
}
function sccharacters_has_player_handle(array $character): bool
{
return trim((string) ($character['cl_sccharacter_player_handle'] ?? '')) !== '';
}
function sccharacters_reindex_character_items(PDO $db, int $character_id): void
{
if ($character_id <= 0) {
return;
}
$stmt = $db->prepare(
'SELECT cl_sccharacteritem_id
FROM tbl_sccharacteritems
WHERE cl_sccharacteritem_character_id = :character_id
ORDER BY
CASE WHEN cl_sccharacteritem_sort_order <= 0 THEN 0 ELSE 1 END,
cl_sccharacteritem_sort_order ASC,
cl_sccharacteritem_id ASC'
);
$stmt->execute(['character_id' => $character_id]);
$item_ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
if ($item_ids === []) {
return;
}
$stmt_update = $db->prepare(
'UPDATE tbl_sccharacteritems
SET cl_sccharacteritem_sort_order = :sort_order
WHERE cl_sccharacteritem_character_id = :character_id
AND cl_sccharacteritem_id = :item_id'
);
$db->beginTransaction();
try {
$position = 1;
foreach ($item_ids as $item_id) {
$stmt_update->execute([
'sort_order' => $position,
'character_id' => $character_id,
'item_id' => $item_id,
]);
$position++;
}
$db->commit();
} catch (Throwable $exception) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $exception;
}
}
function sccharacters_next_item_sort_order(PDO $db, int $character_id): int
{
if ($character_id <= 0) {
return 1;
}
sccharacters_reindex_character_items($db, $character_id);
$stmt = $db->prepare(
'SELECT COALESCE(MAX(cl_sccharacteritem_sort_order), 0)
FROM tbl_sccharacteritems
WHERE cl_sccharacteritem_character_id = :character_id'
);
$stmt->execute(['character_id' => $character_id]);
return ((int) $stmt->fetchColumn()) + 1;
}
function sccharacters_item_category_options(): array
{
return [
'weapon' => 'Armes',
'armor' => 'Armures',
'tools' => 'Outils',
'consumables' => 'Consommables',
'ammunition' => 'Munitions',
'attachments' => 'Accessoires',
'clothing' => 'Vêtements',
'cargo' => 'Cargo / Conteneurs',
'ship' => 'Composants / Véhicule',
'access' => 'Accès / Mobilier',
'misc' => 'Divers',
];
}
function sccharacters_default_category_order(): array
{
return array_keys(sccharacters_item_category_options());
}
function sccharacters_normalize_category_order(array $category_order): array
{
$known_categories = sccharacters_default_category_order();
$known_lookup = array_fill_keys($known_categories, true);
$normalized = [];
foreach ($category_order as $category_key) {
$category_key = trim((string) $category_key);
if ($category_key === '' || !isset($known_lookup[$category_key]) || isset($normalized[$category_key])) {
continue;
}
$normalized[$category_key] = $category_key;
}
foreach ($known_categories as $category_key) {
if (!isset($normalized[$category_key])) {
$normalized[$category_key] = $category_key;
}
}
return array_values($normalized);
}
function sccharacters_parse_category_order(?string $raw_value): array
{
$raw_value = trim((string) $raw_value);
if ($raw_value === '') {
return sccharacters_default_category_order();
}
$decoded = json_decode($raw_value, true);
if (!is_array($decoded)) {
return sccharacters_default_category_order();
}
return sccharacters_normalize_category_order($decoded);
}
function sccharacters_encode_category_order(array $category_order): string
{
$encoded = json_encode(
sccharacters_normalize_category_order($category_order),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
return $encoded !== false
? $encoded
: json_encode(sccharacters_default_category_order(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
function sccharacters_character_category_order(array $character_row): array
{
return sccharacters_parse_category_order((string) ($character_row['cl_sccharacter_category_order'] ?? ''));
}
function sccharacters_sort_items_by_category_order(array $items_by_category, array $category_order): array
{
if ($items_by_category === []) {
return [];
}
$sorted = [];
foreach (sccharacters_normalize_category_order($category_order) as $category_key) {
if (isset($items_by_category[$category_key])) {
$sorted[$category_key] = $items_by_category[$category_key];
}
}
foreach ($items_by_category as $category_key => $category_items) {
if (!isset($sorted[$category_key])) {
$sorted[$category_key] = $category_items;
}
}
return $sorted;
}
function sccharacters_save_character_category_order(PDO $db, int $character_id, array $category_order): void
{
if ($character_id <= 0) {
return;
}
$stmt = $db->prepare(
'UPDATE tbl_sccharacters
SET cl_sccharacter_category_order = :category_order
WHERE cl_sccharacter_id = :character_id'
);
$stmt->execute([
'category_order' => sccharacters_encode_category_order($category_order),
'character_id' => $character_id,
]);
}
function sccharacters_item_category_label(string $category): string
{
$options = sccharacters_item_category_options();
return $options[$category] ?? $options['misc'];
}
function sccharacters_item_category_slug(string $value): string
{
$value = trim($value);
if ($value == '') {
return '';
}
if (function_exists('iconv')) {
$converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
if ($converted !== false) {
$value = $converted;
}
}
$value = strtolower($value);
$value = preg_replace('/[^a-z0-9]+/', '_', $value) ?? '';
return trim($value, '_');
}
function sccharacters_string_contains_any(string $haystack, array $needles): bool
{
foreach ($needles as $needle) {
if ($needle !== '' && strpos($haystack, $needle) !== false) {
return true;
}
}
return false;
}
function sccharacters_normalize_item_category(?string $value): string
{
$slug = sccharacters_item_category_slug((string) $value);
if ($slug === '' || in_array($slug, ['auto', 'automatic', 'automatique', 'default', 'defaut'], true)) {
return '';
}
$options = sccharacters_item_category_options();
if (isset($options[$slug])) {
return $slug;
}
$exact_aliases = [
'arme' => 'weapon',
'armes' => 'weapon',
'weapon' => 'weapon',
'weapons' => 'weapon',
'armure' => 'armor',
'armures' => 'armor',
'armor' => 'armor',
'armors' => 'armor',
'outil' => 'tools',
'outils' => 'tools',
'tool' => 'tools',
'tools' => 'tools',
'consommable' => 'consumables',
'consommables' => 'consumables',
'usable' => 'consumables',
'food' => 'consumables',
'drink' => 'consumables',
'munition' => 'ammunition',
'munitions' => 'ammunition',
'ammo' => 'ammunition',
'ammunition' => 'ammunition',
'accessoire' => 'attachments',
'accessoires' => 'attachments',
'attachment' => 'attachments',
'attachments' => 'attachments',
'vetement' => 'clothing',
'vetements' => 'clothing',
'clothing' => 'clothing',
'clothes' => 'clothing',
'cargo' => 'cargo',
'container' => 'cargo',
'conteneur' => 'cargo',
'conteneurs' => 'cargo',
'composant' => 'ship',
'composants' => 'ship',
'component' => 'ship',
'components' => 'ship',
'vaisseau' => 'ship',
'vaisseaux' => 'ship',
'vehicule' => 'ship',
'vehicules' => 'ship',
'acces' => 'access',
'access' => 'access',
'mobilier' => 'access',
'divers' => 'misc',
'misc' => 'misc',
'autre' => 'misc',
'autres' => 'misc',
'other' => 'misc',
];
if (isset($exact_aliases[$slug])) {
return $exact_aliases[$slug];
}
$contains_aliases = [
'weapon' => ['arme', 'weapon', 'pistol', 'rifle', 'shotgun', 'sniper', 'knife', 'blade'],
'armor' => ['armure', 'armor', 'plating'],
'tools' => ['outil', 'tool', 'tractor', 'mining', 'multitool', 'multi_tool', 'gadget'],
'consumables' => ['consommable', 'consumable', 'usable', 'food', 'drink', 'med', 'medical', 'heal'],
'ammunition' => ['munition', 'ammo', 'ammunition', 'magazine', 'grenade', 'rocket'],
'attachments' => ['attachment', 'accessoire', 'scope', 'optic', 'silencer', 'sight', 'barrel'],
'clothing' => ['clothing', 'vetement', 'apparel', 'outfit', 'char_clothing', 'char_head'],
'cargo' => ['cargo', 'container', 'crate', 'box'],
'ship' => ['component', 'composant', 'vaisseau', 'vehicule', 'thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'missilelauncher', 'docking', 'flightcontroller', 'fueltank', 'fuelintake', 'aimodule'],
'access' => ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'mobilier'],
];
foreach ($contains_aliases as $category => $needles) {
if (sccharacters_string_contains_any($slug, $needles)) {
return $category;
}
}
return '';
}
function sccharacters_guess_item_category(?string $type, ?string $subtype = null): string
{
$haystack = strtolower(trim(((string) $type) . ' ' . ((string) $subtype)));
$slug = sccharacters_item_category_slug($haystack);
if ($slug === '') {
return 'misc';
}
if (sccharacters_string_contains_any($slug, ['char_clothing', 'char_head', 'clothing', 'apparel', 'outfit', 'hat', 'beard', 'piercing'])) {
return 'clothing';
}
if (sccharacters_string_contains_any($slug, ['weaponattachment', 'attachment', 'scope', 'optic', 'sight', 'silencer', 'barrel', 'mag_mount'])) {
return 'attachments';
}
if (sccharacters_string_contains_any($slug, ['missile', 'ammo', 'ammunition', 'magazine', 'rocket'])) {
return 'ammunition';
}
if (sccharacters_string_contains_any($slug, ['armor', 'armure'])) {
return 'armor';
}
if (sccharacters_string_contains_any($slug, ['weapon', 'gun', 'rifle', 'pistol', 'shotgun', 'sniper', 'knife', 'blade'])) {
return 'weapon';
}
if (sccharacters_string_contains_any($slug, ['usable', 'consumable', 'food', 'drink', 'fps_consumable', 'med', 'medical'])) {
return 'consumables';
}
if (sccharacters_string_contains_any($slug, ['tool', 'outil', 'mining', 'tractor', 'gadget', 'utility'])) {
return 'tools';
}
if (sccharacters_string_contains_any($slug, ['cargo', 'container', 'crate', 'box'])) {
return 'cargo';
}
if (sccharacters_string_contains_any($slug, ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'shopdisplay', 'player'])) {
return 'access';
}
if (sccharacters_string_contains_any($slug, ['thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'flightcontroller', 'fueltank', 'fuelintake', 'a_module', 'aimodule', 'docking', 'attachedpart'])) {
return 'ship';
}
return 'misc';
}
function sccharacters_resolve_item_category(?string $storedValue, ?string $type, ?string $subtype = null): string
{
$normalized = sccharacters_normalize_item_category($storedValue);
if ($normalized !== '') {
return $normalized;
}
return sccharacters_guess_item_category($type, $subtype);
}

View File

@ -1,478 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function scdiscord_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scwebhooks (
cl_scwebhook_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scwebhook_name VARCHAR(255) NOT NULL,
cl_scwebhook_url TEXT NOT NULL,
cl_scwebhook_image_url TEXT NOT NULL,
cl_scwebhook_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00',
cl_scwebhook_is_forum TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (cl_scwebhook_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scwebhooks LIKE 'cl_scwebhook_image_url'");
$has_webhook_image = (bool) $columns_stmt->fetch();
if (!$has_webhook_image) {
$db->exec("ALTER TABLE tbl_scwebhooks ADD COLUMN cl_scwebhook_image_url TEXT NOT NULL AFTER cl_scwebhook_url");
}
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scwebhooks LIKE 'cl_scwebhook_border_color'");
$has_webhook_border_color = (bool) $columns_stmt->fetch();
if (!$has_webhook_border_color) {
$db->exec("ALTER TABLE tbl_scwebhooks ADD COLUMN cl_scwebhook_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00' AFTER cl_scwebhook_image_url");
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scbanners (
cl_scbanner_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scbanner_name VARCHAR(255) NOT NULL,
cl_scbanner_url TEXT NOT NULL,
cl_scbanner_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00',
PRIMARY KEY (cl_scbanner_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scbanners LIKE 'cl_scbanner_border_color'");
$has_border_color = (bool) $columns_stmt->fetch();
if (!$has_border_color) {
$db->exec("ALTER TABLE tbl_scbanners ADD COLUMN cl_scbanner_border_color VARCHAR(20) NOT NULL DEFAULT '#ffae00' AFTER cl_scbanner_url");
}
$db->exec(
"UPDATE tbl_scwebhooks w
LEFT JOIN tbl_scbanners b ON b.cl_scbanner_name = w.cl_scwebhook_name
SET
w.cl_scwebhook_image_url = CASE
WHEN (w.cl_scwebhook_image_url IS NULL OR w.cl_scwebhook_image_url = '') AND b.cl_scbanner_url IS NOT NULL THEN b.cl_scbanner_url
ELSE w.cl_scwebhook_image_url
END,
w.cl_scwebhook_border_color = CASE
WHEN (w.cl_scwebhook_border_color IS NULL OR w.cl_scwebhook_border_color = '' OR w.cl_scwebhook_border_color = '#ffae00') AND b.cl_scbanner_border_color IS NOT NULL THEN b.cl_scbanner_border_color
ELSE w.cl_scwebhook_border_color
END"
);
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scnotifications (
cl_scnotification_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scnotification_webhook_id INT(11) NOT NULL,
cl_scnotification_banner_id INT(11) DEFAULT NULL,
cl_scnotification_title VARCHAR(255) NOT NULL DEFAULT '',
cl_scnotification_message TEXT NOT NULL,
cl_scnotification_payload LONGTEXT NOT NULL,
cl_scnotification_response LONGTEXT DEFAULT NULL,
cl_scnotification_success TINYINT(1) NOT NULL DEFAULT 0,
cl_scnotification_created_by VARCHAR(190) NOT NULL,
cl_scnotification_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scnotification_id),
KEY idx_scnotification_webhook (cl_scnotification_webhook_id),
KEY idx_scnotification_banner (cl_scnotification_banner_id),
CONSTRAINT fk_scnotification_webhook FOREIGN KEY (cl_scnotification_webhook_id)
REFERENCES tbl_scwebhooks (cl_scwebhook_id)
ON UPDATE CASCADE
ON DELETE RESTRICT,
CONSTRAINT fk_scnotification_banner FOREIGN KEY (cl_scnotification_banner_id)
REFERENCES tbl_scbanners (cl_scbanner_id)
ON UPDATE CASCADE
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$stmt_existing_red = $db->prepare('SELECT cl_scbanner_id FROM tbl_scbanners WHERE cl_scbanner_name = :name LIMIT 1');
$stmt_existing_red->execute(['name' => 'Alerte Rouge']);
$existing_red_banner = $stmt_existing_red->fetch(PDO::FETCH_ASSOC);
if ($existing_red_banner) {
$stmt_update_red = $db->prepare(
'UPDATE tbl_scbanners
SET cl_scbanner_border_color = CASE
WHEN cl_scbanner_border_color IS NULL OR cl_scbanner_border_color = "" OR cl_scbanner_border_color = "#ffae00"
THEN :border_color
ELSE cl_scbanner_border_color
END
WHERE cl_scbanner_id = :id'
);
$stmt_update_red->execute([
'border_color' => '#ff3b30',
'id' => $existing_red_banner['cl_scbanner_id'],
]);
}
$bootstrapped = true;
}
function scdiscord_mask_webhook_url(string $url): string
{
$trimmed = trim($url);
if ($trimmed === '') {
return '';
}
$length = strlen($trimmed);
if ($length <= 24) {
return str_repeat('•', max(8, $length));
}
return substr($trimmed, 0, 32) . str_repeat('•', 18) . substr($trimmed, -10);
}
function scdiscord_normalize_hex_color(string $color): string
{
$candidate = strtoupper(trim($color));
if ($candidate === '') {
return '#FFAE00';
}
if ($candidate[0] !== '#') {
$candidate = '#' . $candidate;
}
if (!preg_match('/^#[0-9A-F]{6}$/', $candidate)) {
return '#FFAE00';
}
return $candidate;
}
function scdiscord_hex_to_decimal(string $color): int
{
return hexdec(ltrim(scdiscord_normalize_hex_color($color), '#'));
}
function scdiscord_build_mentions(bool $notify_here, bool $notify_everyone): array
{
$parts = [];
if ($notify_here) {
$parts[] = '@here';
}
if ($notify_everyone) {
$parts[] = '@everyone';
}
return $parts;
}
function scdiscord_build_thread_name(string $title, string $location, string $start_date): string
{
$parts = [];
if ($title !== '') {
$parts[] = $title;
}
if ($location !== '') {
$parts[] = $location;
}
if ($start_date !== '') {
$parts[] = $start_date;
}
$thread_name = trim(implode(' • ', $parts));
if ($thread_name === '') {
$thread_name = 'Notification Discord';
}
return mb_substr($thread_name, 0, 100);
}
function scdiscord_post_webhook(string $webhook_url, array $payload): array
{
$target_url = $webhook_url;
$separator = (str_contains($target_url, '?')) ? '&' : '?';
$target_url .= $separator . 'wait=true';
$json_payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json_payload === false) {
return [
'success' => false,
'http_code' => 0,
'response' => 'Erreur d\'encodage JSON.',
];
}
if (function_exists('curl_init')) {
$ch = curl_init($target_url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json_payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($json_payload),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
$http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
return [
'success' => false,
'http_code' => $http_code,
'response' => $curl_error !== '' ? $curl_error : 'Erreur CURL inconnue.',
];
}
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response,
];
}
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => $json_payload,
'timeout' => 20,
'ignore_errors' => true,
],
]);
$response = @file_get_contents($target_url, false, $context);
$http_code = 0;
if (isset($http_response_header) && is_array($http_response_header)) {
foreach ($http_response_header as $header_line) {
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header_line, $matches)) {
$http_code = (int) $matches[1];
break;
}
}
}
return [
'success' => $response !== false && $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response === false ? 'Erreur lors de la requête HTTP.' : $response,
];
}
function scdiscord_get_bot_token(): string
{
$candidates = [];
if (defined('DISCORD_BOT_TOKEN') && is_string(DISCORD_BOT_TOKEN)) {
$candidates[] = DISCORD_BOT_TOKEN;
}
foreach (['DISCORD_BOT_TOKEN', 'SC_DISCORD_BOT_TOKEN', 'BOT_TOKEN'] as $env_key) {
$value = getenv($env_key);
if ($value !== false) {
$candidates[] = $value;
}
}
foreach ($candidates as $candidate) {
$token = trim((string) $candidate);
if ($token !== '') {
return $token;
}
}
return '';
}
function scdiscord_decode_json_response(?string $response): array
{
if (!is_string($response) || trim($response) === '') {
return [];
}
$decoded = json_decode($response, true);
return is_array($decoded) ? $decoded : [];
}
function scdiscord_bot_request(string $method, string $url, string $bot_token, ?array $payload = null): array
{
$headers = [
'Authorization: Bot ' . $bot_token,
];
$json_payload = null;
if ($payload !== null) {
$json_payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json_payload === false) {
return [
'success' => false,
'http_code' => 0,
'response' => 'Erreur d\'encodage JSON.',
];
}
$headers[] = 'Content-Type: application/json';
$headers[] = 'Content-Length: ' . strlen($json_payload);
}
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
if ($json_payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);
}
$response = curl_exec($ch);
$http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
return [
'success' => false,
'http_code' => $http_code,
'response' => $curl_error !== '' ? $curl_error : 'Erreur CURL inconnue.',
];
}
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response,
];
}
$header_lines = implode("\r\n", $headers) . "\r\n";
$context = stream_context_create([
'http' => [
'method' => strtoupper($method),
'header' => $header_lines,
'content' => $json_payload ?? '',
'timeout' => 20,
'ignore_errors' => true,
],
]);
$response = @file_get_contents($url, false, $context);
$http_code = 0;
if (isset($http_response_header) && is_array($http_response_header)) {
foreach ($http_response_header as $header_line) {
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header_line, $matches)) {
$http_code = (int) $matches[1];
break;
}
}
}
return [
'success' => $response !== false && $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'response' => $response === false ? 'Erreur lors de la requête HTTP.' : $response,
];
}
function scdiscord_apply_bot_actions(array $message_data, bool $use_reactions, bool $use_publicthread, string $thread_name): array
{
if (!$use_reactions && !$use_publicthread) {
return [
'success' => true,
'http_code' => 200,
'response' => 'Aucune action bot demandée.',
'details' => [],
];
}
$message_id = trim((string) ($message_data['id'] ?? ''));
$channel_id = trim((string) ($message_data['channel_id'] ?? ''));
if ($message_id === '' || $channel_id === '') {
return [
'success' => false,
'http_code' => 0,
'response' => 'Réponse Discord invalide : id de message ou channel_id manquant.',
'details' => [],
];
}
$bot_token = scdiscord_get_bot_token();
if ($bot_token === '') {
return [
'success' => false,
'http_code' => 0,
'response' => 'Token bot Discord manquant. Définis DISCORD_BOT_TOKEN côté serveur.',
'details' => [],
];
}
$details = [];
$last_http_code = 200;
$failed = false;
if ($use_reactions) {
foreach (['👍', '⌛', '❔', '👎'] as $emoji) {
$url = 'https://discord.com/api/v10/channels/' . rawurlencode($channel_id) . '/messages/' . rawurlencode($message_id) . '/reactions/' . rawurlencode($emoji) . '/@me';
$result = scdiscord_bot_request('PUT', $url, $bot_token);
$action_success = !empty($result['success']);
$last_http_code = (int) ($result['http_code'] ?? $last_http_code);
$details[] = [
'action' => 'reaction',
'emoji' => $emoji,
'success' => $action_success,
'http_code' => $last_http_code,
'response' => (string) ($result['response'] ?? ''),
];
if (!$action_success) {
$failed = true;
}
sleep(1);
}
}
if ($use_publicthread) {
$thread_payload = [
'name' => $thread_name !== '' ? $thread_name : 'Discussion - Opération',
'auto_archive_duration' => 1440,
'type' => 11,
];
$url = 'https://discord.com/api/v10/channels/' . rawurlencode($channel_id) . '/messages/' . rawurlencode($message_id) . '/threads';
$result = scdiscord_bot_request('POST', $url, $bot_token, $thread_payload);
$action_success = !empty($result['success']);
$last_http_code = (int) ($result['http_code'] ?? $last_http_code);
$details[] = [
'action' => 'thread',
'success' => $action_success,
'http_code' => $last_http_code,
'response' => (string) ($result['response'] ?? ''),
];
if (!$action_success) {
$failed = true;
}
}
return [
'success' => !$failed,
'http_code' => $failed ? $last_http_code : 200,
'response' => json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'details' => $details,
];
}

View File

@ -1,139 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function scitemcustom_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function scitemcustom_column_definition(PDO $db, string $table, string $column): ?array
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
$column_definition = $stmt->fetch(PDO::FETCH_ASSOC);
return $column_definition ?: null;
}
function scitemcustom_index_exists(PDO $db, string $table, string $index): bool
{
$stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
return (bool) $stmt->fetch();
}
function scitemcustom_foreign_key_exists(PDO $db, string $table, string $constraint): bool
{
$stmt = $db->prepare(
"SELECT COUNT(*)
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND CONSTRAINT_NAME = :constraint_name
AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
);
$stmt->execute([
'table_name' => $table,
'constraint_name' => $constraint,
]);
return (int) $stmt->fetchColumn() > 0;
}
function scitemcustom_bootstrap(): void
{
static $scitemcustom_bootstrap_done = false;
if ($scitemcustom_bootstrap_done) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scitemcustom (
cl_scitemcustom_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scitemcustom_owner_auth_id INT UNSIGNED NOT NULL,
cl_scitemcustom_obj_id INT(10) UNSIGNED NOT NULL,
cl_scitemcustom_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scitemcustom_id),
KEY idx_scitemcustom_owner (cl_scitemcustom_owner_auth_id),
KEY idx_scitemcustom_obj (cl_scitemcustom_obj_id),
CONSTRAINT fk_scitemcustom_owner_auth FOREIGN KEY (cl_scitemcustom_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_scitemcustom_obj FOREIGN KEY (cl_scitemcustom_obj_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scitemcustom_column_exists($db, 'tbl_scitemcustom', 'cl_scitemcustom_owner_auth_id')) {
$db->exec(
'ALTER TABLE tbl_scitemcustom
ADD COLUMN cl_scitemcustom_owner_auth_id INT UNSIGNED NULL AFTER cl_scitemcustom_id'
);
}
if (scitemcustom_index_exists($db, 'tbl_scitemcustom', 'uq_scitemcustom_obj')) {
$db->exec('ALTER TABLE tbl_scitemcustom DROP INDEX uq_scitemcustom_obj');
}
if (!scitemcustom_index_exists($db, 'tbl_scitemcustom', 'idx_scitemcustom_owner')) {
$db->exec(
'ALTER TABLE tbl_scitemcustom
ADD INDEX idx_scitemcustom_owner (cl_scitemcustom_owner_auth_id)'
);
}
if (scitemcustom_index_exists($db, 'tbl_scitemcustom', 'uq_scitemcustom_owner_obj')) {
$db->exec('ALTER TABLE tbl_scitemcustom DROP INDEX uq_scitemcustom_owner_obj');
}
if (!scitemcustom_foreign_key_exists($db, 'tbl_scitemcustom', 'fk_scitemcustom_owner_auth')) {
$db->exec(
'ALTER TABLE tbl_scitemcustom
ADD CONSTRAINT fk_scitemcustom_owner_auth FOREIGN KEY (cl_scitemcustom_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scitemcustomstat (
cl_scitemcustomstat_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scitemcustomstat_itemcustom_id INT(11) NOT NULL,
cl_scitemcustomstat_stat_id INT(11) NOT NULL,
cl_scitemcustomstat_sign ENUM('+', '', '-') NOT NULL DEFAULT '+',
cl_scitemcustomstat_value DECIMAL(10,2) NOT NULL DEFAULT 0.00,
cl_scitemcustomstat_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scitemcustomstat_id),
UNIQUE KEY uq_scitemcustomstat_item_stat (cl_scitemcustomstat_itemcustom_id, cl_scitemcustomstat_stat_id),
KEY idx_scitemcustomstat_item (cl_scitemcustomstat_itemcustom_id),
KEY idx_scitemcustomstat_stat (cl_scitemcustomstat_stat_id),
CONSTRAINT fk_scitemcustomstat_item FOREIGN KEY (cl_scitemcustomstat_itemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_scitemcustomstat_stat FOREIGN KEY (cl_scitemcustomstat_stat_id)
REFERENCES tbl_scstatsitem (cl_scstatsitem_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
$sign_column = scitemcustom_column_definition($db, 'tbl_scitemcustomstat', 'cl_scitemcustomstat_sign');
if ($sign_column && ($sign_column['Type'] ?? '') !== "enum('+','','-')") {
$db->exec(
"ALTER TABLE tbl_scitemcustomstat
MODIFY COLUMN cl_scitemcustomstat_sign ENUM('+', '', '-') NOT NULL DEFAULT '+'"
);
}
$scitemcustom_bootstrap_done = true;
}

View File

@ -1,45 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function scitems_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function scitems_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scobjs (
cl_scobjs_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
cl_scobjs_name VARCHAR(255) NOT NULL,
cl_scobjs_type VARCHAR(100) DEFAULT NULL,
cl_scobjs_subtype VARCHAR(100) DEFAULT NULL,
cl_scobjs_uuid VARCHAR(100) NOT NULL,
cl_scobjs_rarity VARCHAR(10) DEFAULT '',
cl_scobjs_about TEXT DEFAULT NULL,
cl_scobjs_description TEXT DEFAULT NULL,
PRIMARY KEY (cl_scobjs_id),
UNIQUE KEY cl_scobjs_uuid (cl_scobjs_uuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scitems_column_exists($db, 'tbl_scobjs', 'cl_scobjs_description')) {
$db->exec(
'ALTER TABLE tbl_scobjs
ADD COLUMN cl_scobjs_description TEXT DEFAULT NULL AFTER cl_scobjs_about'
);
}
$bootstrapped = true;
}

View File

@ -1,801 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function scmanutention_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
return (bool) $stmt->fetch();
}
function scmanutention_index_exists(PDO $db, string $table, string $index): bool
{
$stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
return (bool) $stmt->fetch();
}
function scmanutention_foreign_key_exists(PDO $db, string $table, string $constraint): bool
{
$stmt = $db->prepare(
"SELECT COUNT(*)
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND CONSTRAINT_NAME = :constraint_name
AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
);
$stmt->execute([
'table_name' => $table,
'constraint_name' => $constraint,
]);
return (int) $stmt->fetchColumn() > 0;
}
function scmanutention_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scmanutentions (
cl_scmanutention_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_scmanutention_owner_auth_id INT UNSIGNED NOT NULL,
cl_scmanutention_title VARCHAR(190) NOT NULL,
cl_scmanutention_type VARCHAR(120) NOT NULL DEFAULT '',
cl_scmanutention_subtype VARCHAR(120) NOT NULL DEFAULT '',
cl_scmanutention_description TEXT DEFAULT NULL,
cl_scmanutention_share_token VARCHAR(64) NOT NULL,
cl_scmanutention_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
cl_scmanutention_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cl_scmanutention_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scmanutention_id),
UNIQUE KEY uq_scmanutention_share_token (cl_scmanutention_share_token),
KEY idx_scmanutention_owner (cl_scmanutention_owner_auth_id),
KEY idx_scmanutention_title (cl_scmanutention_title),
CONSTRAINT fk_scmanutention_owner_auth FOREIGN KEY (cl_scmanutention_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_owner_auth_id')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_owner_auth_id INT UNSIGNED NULL AFTER cl_scmanutention_id'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_type')) {
$db->exec(
"ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_type VARCHAR(120) NOT NULL DEFAULT '' AFTER cl_scmanutention_title"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_subtype')) {
$db->exec(
"ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_subtype VARCHAR(120) NOT NULL DEFAULT '' AFTER cl_scmanutention_type"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_description')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_description TEXT NULL AFTER cl_scmanutention_subtype'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_share_token')) {
$db->exec(
"ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_share_token VARCHAR(64) NOT NULL DEFAULT '' AFTER cl_scmanutention_description"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentions', 'cl_scmanutention_share_enabled')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD COLUMN cl_scmanutention_share_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_scmanutention_share_token'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentions', 'idx_scmanutention_owner')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD INDEX idx_scmanutention_owner (cl_scmanutention_owner_auth_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentions', 'idx_scmanutention_title')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD INDEX idx_scmanutention_title (cl_scmanutention_title)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentions', 'uq_scmanutention_share_token')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD UNIQUE KEY uq_scmanutention_share_token (cl_scmanutention_share_token)'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentions', 'fk_scmanutention_owner_auth')) {
$db->exec(
'ALTER TABLE tbl_scmanutentions
ADD CONSTRAINT fk_scmanutention_owner_auth FOREIGN KEY (cl_scmanutention_owner_auth_id)
REFERENCES tbl_auth (cl_auth_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scmanutentionitems (
cl_scmanutentionitem_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cl_scmanutentionitem_manutention_id INT UNSIGNED NOT NULL,
cl_scmanutentionitem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
cl_scmanutentionitem_scobjs_id INT UNSIGNED DEFAULT NULL,
cl_scmanutentionitem_scitemcustom_id INT(11) DEFAULT NULL,
cl_scmanutentionitem_quantity INT UNSIGNED NOT NULL DEFAULT 1,
cl_scmanutentionitem_extra_info TEXT DEFAULT NULL,
cl_scmanutentionitem_sort_order INT UNSIGNED NOT NULL DEFAULT 0,
cl_scmanutentionitem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cl_scmanutentionitem_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scmanutentionitem_id),
KEY idx_scmanutentionitem_sheet (cl_scmanutentionitem_manutention_id),
KEY idx_scmanutentionitem_scobjs (cl_scmanutentionitem_scobjs_id),
KEY idx_scmanutentionitem_scitemcustom (cl_scmanutentionitem_scitemcustom_id),
KEY idx_scmanutentionitem_sheet_sort (cl_scmanutentionitem_manutention_id, cl_scmanutentionitem_sort_order, cl_scmanutentionitem_id),
CONSTRAINT fk_scmanutentionitem_sheet FOREIGN KEY (cl_scmanutentionitem_manutention_id)
REFERENCES tbl_scmanutentions (cl_scmanutention_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_scmanutentionitem_scobjs FOREIGN KEY (cl_scmanutentionitem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE,
CONSTRAINT fk_scmanutentionitem_scitemcustom FOREIGN KEY (cl_scmanutentionitem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_source')) {
$db->exec(
"ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base' AFTER cl_scmanutentionitem_manutention_id"
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_scobjs_id')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_scobjs_id INT UNSIGNED NULL AFTER cl_scmanutentionitem_source'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_scitemcustom_id')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_scitemcustom_id INT(11) NULL AFTER cl_scmanutentionitem_scobjs_id'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_quantity')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_quantity INT UNSIGNED NOT NULL DEFAULT 1 AFTER cl_scmanutentionitem_scitemcustom_id'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_extra_info')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_extra_info TEXT NULL AFTER cl_scmanutentionitem_quantity'
);
}
if (!scmanutention_column_exists($db, 'tbl_scmanutentionitems', 'cl_scmanutentionitem_sort_order')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD COLUMN cl_scmanutentionitem_sort_order INT UNSIGNED NOT NULL DEFAULT 0 AFTER cl_scmanutentionitem_extra_info'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_sheet')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_sheet (cl_scmanutentionitem_manutention_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_scobjs (cl_scmanutentionitem_scobjs_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_scitemcustom (cl_scmanutentionitem_scitemcustom_id)'
);
}
if (!scmanutention_index_exists($db, 'tbl_scmanutentionitems', 'idx_scmanutentionitem_sheet_sort')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD INDEX idx_scmanutentionitem_sheet_sort (cl_scmanutentionitem_manutention_id, cl_scmanutentionitem_sort_order, cl_scmanutentionitem_id)'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentionitems', 'fk_scmanutentionitem_sheet')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD CONSTRAINT fk_scmanutentionitem_sheet FOREIGN KEY (cl_scmanutentionitem_manutention_id)
REFERENCES tbl_scmanutentions (cl_scmanutention_id)
ON DELETE CASCADE
ON UPDATE CASCADE'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentionitems', 'fk_scmanutentionitem_scobjs')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD CONSTRAINT fk_scmanutentionitem_scobjs FOREIGN KEY (cl_scmanutentionitem_scobjs_id)
REFERENCES tbl_scobjs (cl_scobjs_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
if (!scmanutention_foreign_key_exists($db, 'tbl_scmanutentionitems', 'fk_scmanutentionitem_scitemcustom')) {
$db->exec(
'ALTER TABLE tbl_scmanutentionitems
ADD CONSTRAINT fk_scmanutentionitem_scitemcustom FOREIGN KEY (cl_scmanutentionitem_scitemcustom_id)
REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
ON DELETE SET NULL
ON UPDATE CASCADE'
);
}
$stmt_missing_tokens = $db->query(
"SELECT cl_scmanutention_id
FROM tbl_scmanutentions
WHERE cl_scmanutention_share_token = ''
OR cl_scmanutention_share_token IS NULL"
);
foreach ($stmt_missing_tokens->fetchAll(PDO::FETCH_COLUMN) as $sheet_id) {
$stmt_update = $db->prepare(
'UPDATE tbl_scmanutentions
SET cl_scmanutention_share_token = :token
WHERE cl_scmanutention_id = :id'
);
$stmt_update->execute([
'token' => scmanutention_generate_share_token(),
'id' => (int) $sheet_id,
]);
}
$bootstrapped = true;
}
function scmanutention_generate_share_token(int $length = 32): string
{
$length = max(16, min(64, $length));
return bin2hex(random_bytes((int) ceil($length / 2)));
}
function scmanutention_clean_text(?string $value): string
{
return trim((string) $value);
}
function scmanutention_normalize_quantity($value): int
{
$quantity = (int) $value;
if ($quantity <= 0) {
return 1;
}
return min($quantity, 999999);
}
function scmanutention_is_valid_source(string $value): bool
{
return in_array($value, ['base', 'custom'], true);
}
function scmanutention_escape_like(string $value): string
{
return strtr($value, [
'\\' => '\\\\',
'%' => '\\%',
'_' => '\\_',
]);
}
function scmanutention_sheet_share_url(string $token): string
{
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
|| ((string) ($_SERVER['SERVER_PORT'] ?? '') === '443');
$scheme = $is_https ? 'https' : 'http';
$host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '') {
$host = '127.0.0.1';
}
return $scheme . '://' . $host . '/scmanutentionpublic.php?share=' . rawurlencode($token);
}
function scmanutention_find_owned_sheet(PDO $db, int $sheet_id, int $owner_auth_id): ?array
{
if ($sheet_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
'SELECT *
FROM tbl_scmanutentions
WHERE cl_scmanutention_id = :id
AND cl_scmanutention_owner_auth_id = :owner_auth_id
LIMIT 1'
);
$stmt->execute([
'id' => $sheet_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
return $row ?: null;
}
function scmanutention_find_public_sheet_by_token(PDO $db, string $share_token): ?array
{
$share_token = trim($share_token);
if ($share_token === '') {
return null;
}
$stmt = $db->prepare(
'SELECT m.*, COALESCE(NULLIF(TRIM(a.cl_auth_user), \'\'), \'Inconnu\') AS cl_scmanutention_owner_name
FROM tbl_scmanutentions m
LEFT JOIN tbl_auth a ON a.cl_auth_id = m.cl_scmanutention_owner_auth_id
WHERE m.cl_scmanutention_share_token = :share_token
AND m.cl_scmanutention_share_enabled = 1
LIMIT 1'
);
$stmt->execute(['share_token' => $share_token]);
$row = $stmt->fetch();
return $row ?: null;
}
function scmanutention_next_item_sort_order(PDO $db, int $sheet_id): int
{
$stmt = $db->prepare(
'SELECT COALESCE(MAX(cl_scmanutentionitem_sort_order), 0)
FROM tbl_scmanutentionitems
WHERE cl_scmanutentionitem_manutention_id = :sheet_id'
);
$stmt->execute(['sheet_id' => $sheet_id]);
return ((int) $stmt->fetchColumn()) + 1;
}
function scmanutention_reindex_items(PDO $db, int $sheet_id): void
{
if ($sheet_id <= 0) {
return;
}
$stmt = $db->prepare(
'SELECT cl_scmanutentionitem_id
FROM tbl_scmanutentionitems
WHERE cl_scmanutentionitem_manutention_id = :sheet_id
ORDER BY cl_scmanutentionitem_sort_order ASC, cl_scmanutentionitem_id ASC'
);
$stmt->execute(['sheet_id' => $sheet_id]);
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$position = 1;
$stmt_update = $db->prepare(
'UPDATE tbl_scmanutentionitems
SET cl_scmanutentionitem_sort_order = :sort_order
WHERE cl_scmanutentionitem_id = :id'
);
foreach ($ids as $id) {
$stmt_update->execute([
'sort_order' => $position++,
'id' => (int) $id,
]);
}
}
function scmanutention_find_owned_item(PDO $db, int $item_id, int $owner_auth_id): ?array
{
if ($item_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
"SELECT
mi.*,
m.cl_scmanutention_owner_auth_id
FROM tbl_scmanutentionitems mi
INNER JOIN tbl_scmanutentions m ON m.cl_scmanutention_id = mi.cl_scmanutentionitem_manutention_id
WHERE mi.cl_scmanutentionitem_id = :item_id
AND m.cl_scmanutention_owner_auth_id = :owner_auth_id
LIMIT 1"
);
$stmt->execute([
'item_id' => $item_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
return $row ?: null;
}
function scmanutention_validate_item_reference(PDO $db, int $owner_auth_id, string $source, int $scobjs_id, int $scitemcustom_id): ?array
{
if (!scmanutention_is_valid_source($source)) {
return null;
}
if ($source === 'base') {
if ($scobjs_id <= 0) {
return null;
}
$stmt = $db->prepare(
'SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid, cl_scobjs_rarity
FROM tbl_scobjs
WHERE cl_scobjs_id = :id
LIMIT 1'
);
$stmt->execute(['id' => $scobjs_id]);
$row = $stmt->fetch();
if (!$row) {
return null;
}
return [
'source' => 'base',
'scobjs_id' => (int) $row['cl_scobjs_id'],
'scitemcustom_id' => 0,
'name' => (string) $row['cl_scobjs_name'],
'type' => (string) ($row['cl_scobjs_type'] ?? ''),
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
'uuid' => (string) ($row['cl_scobjs_uuid'] ?? ''),
'rarity' => (string) ($row['cl_scobjs_rarity'] ?? ''),
];
}
if ($scitemcustom_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
"SELECT
c.cl_scitemcustom_id,
o.cl_scobjs_id,
o.cl_scobjs_name,
o.cl_scobjs_type,
o.cl_scobjs_subtype,
o.cl_scobjs_uuid,
o.cl_scobjs_rarity
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
WHERE c.cl_scitemcustom_id = :itemcustom_id
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id
LIMIT 1"
);
$stmt->execute([
'itemcustom_id' => $scitemcustom_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
if (!$row) {
return null;
}
return [
'source' => 'custom',
'scobjs_id' => (int) $row['cl_scobjs_id'],
'scitemcustom_id' => (int) $row['cl_scitemcustom_id'],
'name' => (string) $row['cl_scobjs_name'],
'type' => (string) ($row['cl_scobjs_type'] ?? ''),
'subtype' => (string) ($row['cl_scobjs_subtype'] ?? ''),
'uuid' => (string) ($row['cl_scobjs_uuid'] ?? ''),
'rarity' => (string) ($row['cl_scobjs_rarity'] ?? ''),
];
}
function scmanutention_search_available_items(PDO $db, int $owner_auth_id, string $query, ?int $limit = 25, int $offset = 0): array
{
$query = trim($query);
if ($query === '') {
return [];
}
$escaped = scmanutention_escape_like($query);
$exact = $escaped;
$prefix = $escaped . '%';
$contains = '%' . $escaped . '%';
$limit_clause = '';
$offset = max(0, $offset);
if ($limit !== null && $limit > 0) {
$limit = max(1, min(100, $limit));
$limit_clause = ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset;
}
$sql = "
SELECT *
FROM (
SELECT
CONCAT('base:', o.cl_scobjs_id) AS result_key,
'base' AS result_source,
o.cl_scobjs_id AS result_scobjs_id,
NULL AS result_scitemcustom_id,
o.cl_scobjs_name AS result_name,
COALESCE(o.cl_scobjs_type, '') AS result_type,
COALESCE(o.cl_scobjs_subtype, '') AS result_subtype,
COALESCE(o.cl_scobjs_uuid, '') AS result_uuid,
COALESCE(o.cl_scobjs_rarity, '') AS result_rarity
FROM tbl_scobjs o
WHERE (
o.cl_scobjs_name LIKE :contains_name_base
OR o.cl_scobjs_type LIKE :contains_type_base
OR o.cl_scobjs_subtype LIKE :contains_subtype_base
OR o.cl_scobjs_uuid LIKE :contains_uuid_base
)
UNION ALL
SELECT
CONCAT('custom:', c.cl_scitemcustom_id) AS result_key,
'custom' AS result_source,
o.cl_scobjs_id AS result_scobjs_id,
c.cl_scitemcustom_id AS result_scitemcustom_id,
o.cl_scobjs_name AS result_name,
COALESCE(o.cl_scobjs_type, '') AS result_type,
COALESCE(o.cl_scobjs_subtype, '') AS result_subtype,
COALESCE(o.cl_scobjs_uuid, '') AS result_uuid,
COALESCE(o.cl_scobjs_rarity, '') AS result_rarity
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id
AND (
o.cl_scobjs_name LIKE :contains_name_custom
OR o.cl_scobjs_type LIKE :contains_type_custom
OR o.cl_scobjs_subtype LIKE :contains_subtype_custom
OR o.cl_scobjs_uuid LIKE :contains_uuid_custom
)
) search_results
ORDER BY
CASE
WHEN result_name = :exact_name THEN 0
WHEN result_name LIKE :prefix_name THEN 1
WHEN result_source = 'custom' THEN 2
WHEN result_uuid = :exact_uuid THEN 3
WHEN result_uuid LIKE :prefix_uuid THEN 4
WHEN result_type LIKE :prefix_type THEN 5
WHEN result_subtype LIKE :prefix_subtype THEN 6
ELSE 7
END ASC,
CHAR_LENGTH(result_name) ASC,
result_name ASC,
result_key ASC
{$limit_clause}";
$stmt = $db->prepare($sql);
$stmt->execute([
'owner_auth_id' => $owner_auth_id,
'contains_name_base' => $contains,
'contains_type_base' => $contains,
'contains_subtype_base' => $contains,
'contains_uuid_base' => $contains,
'contains_name_custom' => $contains,
'contains_type_custom' => $contains,
'contains_subtype_custom' => $contains,
'contains_uuid_custom' => $contains,
'exact_name' => $exact,
'prefix_name' => $prefix,
'exact_uuid' => $exact,
'prefix_uuid' => $prefix,
'prefix_type' => $prefix,
'prefix_subtype' => $prefix,
]);
return $stmt->fetchAll() ?: [];
}
function scmanutention_sortable_item_name(array $item_row): string
{
$name = (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_name'] ?? '')
: ($item_row['cl_scmanutentionitem_base_name'] ?? ''));
$name = trim($name);
if ($name === '') {
return '';
}
if (function_exists('iconv')) {
$ascii_name = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name);
if ($ascii_name !== false) {
$name = $ascii_name;
}
}
return function_exists('mb_strtolower')
? mb_strtolower($name, 'UTF-8')
: strtolower($name);
}
function scmanutention_fetch_items(PDO $db, int $sheet_id): array
{
if ($sheet_id <= 0) {
return [];
}
scmanutention_reindex_items($db, $sheet_id);
$stmt = $db->prepare(
"SELECT
mi.*,
bo.cl_scobjs_name AS cl_scmanutentionitem_base_name,
bo.cl_scobjs_type AS cl_scmanutentionitem_base_type,
bo.cl_scobjs_subtype AS cl_scmanutentionitem_base_subtype,
bo.cl_scobjs_uuid AS cl_scmanutentionitem_base_uuid,
bo.cl_scobjs_rarity AS cl_scmanutentionitem_base_rarity,
co.cl_scitemcustom_id AS cl_scmanutentionitem_custom_ref_id,
oo.cl_scobjs_name AS cl_scmanutentionitem_custom_name,
oo.cl_scobjs_type AS cl_scmanutentionitem_custom_type,
oo.cl_scobjs_subtype AS cl_scmanutentionitem_custom_subtype,
oo.cl_scobjs_uuid AS cl_scmanutentionitem_custom_uuid,
oo.cl_scobjs_rarity AS cl_scmanutentionitem_custom_rarity
FROM tbl_scmanutentionitems mi
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = mi.cl_scmanutentionitem_scobjs_id
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = mi.cl_scmanutentionitem_scitemcustom_id
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
WHERE mi.cl_scmanutentionitem_manutention_id = :sheet_id
ORDER BY mi.cl_scmanutentionitem_sort_order ASC, mi.cl_scmanutentionitem_id ASC"
);
$stmt->execute(['sheet_id' => $sheet_id]);
$items = $stmt->fetchAll() ?: [];
foreach ($items as $index => &$item_row) {
$item_row['__alpha_sort_name'] = scmanutention_sortable_item_name($item_row);
$item_row['__alpha_sort_index'] = $index;
}
unset($item_row);
usort($items, static function (array $left, array $right): int {
$name_compare = strnatcasecmp((string) ($left['__alpha_sort_name'] ?? ''), (string) ($right['__alpha_sort_name'] ?? ''));
if ($name_compare !== 0) {
return $name_compare;
}
$sort_order_compare = ((int) ($left['cl_scmanutentionitem_sort_order'] ?? 0)) <=> ((int) ($right['cl_scmanutentionitem_sort_order'] ?? 0));
if ($sort_order_compare !== 0) {
return $sort_order_compare;
}
$id_compare = ((int) ($left['cl_scmanutentionitem_id'] ?? 0)) <=> ((int) ($right['cl_scmanutentionitem_id'] ?? 0));
if ($id_compare !== 0) {
return $id_compare;
}
return ((int) ($left['__alpha_sort_index'] ?? 0)) <=> ((int) ($right['__alpha_sort_index'] ?? 0));
});
foreach ($items as &$item_row) {
unset($item_row['__alpha_sort_name'], $item_row['__alpha_sort_index']);
}
unset($item_row);
return $items;
}
function scmanutention_fetch_custom_stats_map(PDO $db, array $item_rows): array
{
$custom_ids = [];
foreach ($item_rows as $row) {
if (($row['cl_scmanutentionitem_source'] ?? '') === 'custom' && !empty($row['cl_scmanutentionitem_scitemcustom_id'])) {
$custom_ids[] = (int) $row['cl_scmanutentionitem_scitemcustom_id'];
}
}
$custom_ids = array_values(array_unique(array_filter($custom_ids)));
if ($custom_ids === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($custom_ids), '?'));
$stmt = $db->prepare(
"SELECT
cs.cl_scitemcustomstat_itemcustom_id,
st.cl_scstatsitem_name,
st.cl_scstatsitem_unit,
cs.cl_scitemcustomstat_sign,
cs.cl_scitemcustomstat_value
FROM tbl_scitemcustomstat cs
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt->execute($custom_ids);
$stats_map = [];
foreach ($stmt->fetchAll() as $row) {
$itemcustom_id = (int) $row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($stats_map[$itemcustom_id])) {
$stats_map[$itemcustom_id] = [];
}
$stats_map[$itemcustom_id][] = $row;
}
return $stats_map;
}
function scmanutention_fetch_custom_stats_preview_map(PDO $db, array $custom_ids): array
{
$custom_ids = array_values(array_unique(array_map('intval', array_filter($custom_ids))));
if ($custom_ids === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($custom_ids), '?'));
$stmt = $db->prepare(
"SELECT
cs.cl_scitemcustomstat_itemcustom_id,
st.cl_scstatsitem_name,
st.cl_scstatsitem_unit,
cs.cl_scitemcustomstat_sign,
cs.cl_scitemcustomstat_value
FROM tbl_scitemcustomstat cs
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt->execute($custom_ids);
$stats_map = [];
foreach ($stmt->fetchAll() as $row) {
$itemcustom_id = (int) $row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($stats_map[$itemcustom_id])) {
$stats_map[$itemcustom_id] = [];
}
$stats_map[$itemcustom_id][] = $row;
}
return $stats_map;
}

View File

@ -1,32 +0,0 @@
<?php
require_once __DIR__ . '/config.php';
function scstatsitem_bootstrap(): void
{
static $bootstrapped = false;
if ($bootstrapped) {
return;
}
$db = db();
$db->exec(
"CREATE TABLE IF NOT EXISTS tbl_scstatsitem (
cl_scstatsitem_id INT(11) NOT NULL AUTO_INCREMENT,
cl_scstatsitem_name VARCHAR(255) NOT NULL,
cl_scstatsitem_unit VARCHAR(10) NOT NULL DEFAULT '%',
cl_scstatsitem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cl_scstatsitem_id),
UNIQUE KEY uq_scstatsitem_name (cl_scstatsitem_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
);
$columns_stmt = $db->query("SHOW COLUMNS FROM tbl_scstatsitem LIKE 'cl_scstatsitem_unit'");
$has_unit = (bool) $columns_stmt->fetch();
if (!$has_unit) {
$db->exec("ALTER TABLE tbl_scstatsitem ADD COLUMN cl_scstatsitem_unit VARCHAR(10) NOT NULL DEFAULT '%' AFTER cl_scstatsitem_name");
}
$bootstrapped = true;
}

File diff suppressed because it is too large Load Diff

3015
index.php

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
<?php
$config = [
'upload_max_filesize',
'post_max_size',
'max_execution_time',
'max_input_time',
'session.gc_maxlifetime',
'session.save_path',
'session.cookie_lifetime'
];
foreach ($config as $c) {
echo "$c: " . ini_get($c) . "\n";
}
?>

View File

@ -1,134 +0,0 @@
(function () {
var loginForm = document.querySelector('.js-login-form');
var accountPanel = document.getElementById('accountPanel');
var accountLabel = document.getElementById('accountLabel');
var accountActions = document.getElementById('accountActions');
var adminLink = document.getElementById('adminLink');
var logoutLink = document.getElementById('logoutLink');
var loginStatus = document.getElementById('loginStatus');
var loginModal = document.getElementById('modal-Login');
var overlay = document.querySelector('.md-overlay');
if (!loginForm || !accountPanel || !accountLabel || !accountActions || !logoutLink) {
return;
}
function setStatus(message, isError) {
if (!loginStatus) {
return;
}
loginStatus.textContent = message || '';
loginStatus.classList.toggle('is-error', !!isError);
loginStatus.classList.toggle('is-success', !isError && !!message);
}
function closeModal() {
if (loginModal) {
loginModal.classList.remove('md-show');
}
if (document.documentElement) {
document.documentElement.classList.remove('md-perspective');
}
if (overlay) {
overlay.removeEventListener('click', closeModal);
}
}
function renderAuthenticated(user, role, adminUrl, logoutUrl) {
accountPanel.classList.add('is-authenticated');
accountPanel.classList.remove('md-trigger');
accountPanel.removeAttribute('data-modal');
accountLabel.textContent = 'Bonjour, ' + user;
accountActions.hidden = false;
if (role === 'admin' && adminUrl) {
adminLink.href = adminUrl;
adminLink.hidden = false;
} else {
adminLink.hidden = true;
}
if (logoutUrl) {
logoutLink.href = logoutUrl;
}
}
function renderLoggedOut(defaultLabel) {
accountPanel.classList.remove('is-authenticated');
accountPanel.classList.add('md-trigger');
accountPanel.setAttribute('data-modal', 'modal-Login');
accountLabel.textContent = defaultLabel;
accountActions.hidden = true;
adminLink.hidden = true;
}
accountPanel.addEventListener('click', function (event) {
if (!accountPanel.classList.contains('is-authenticated')) {
return;
}
event.stopImmediatePropagation();
}, true);
loginForm.addEventListener('submit', function (event) {
event.preventDefault();
setStatus('', false);
var formData = new FormData(loginForm);
fetch('login.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(function (response) {
return response.json().then(function (payload) {
return {
ok: response.ok,
payload: payload
};
});
})
.then(function (result) {
if (!result.ok || !result.payload.success) {
throw new Error(result.payload.message || 'Connexion impossible.');
}
renderAuthenticated(result.payload.user, result.payload.role, result.payload.adminUrl, result.payload.logoutUrl);
setStatus(result.payload.message || 'Connexion réussie.', false);
loginForm.reset();
window.setTimeout(function () {
closeModal();
window.location.reload();
}, 250);
})
.catch(function (error) {
setStatus(error.message || 'Connexion impossible.', true);
});
});
logoutLink.addEventListener('click', function (event) {
event.preventDefault();
fetch(logoutLink.href, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function (response) {
return response.json();
})
.then(function () {
renderLoggedOut(accountPanel.dataset.loginLabel || 'Connexion');
setStatus('', false);
window.location.reload();
})
.catch(function () {
window.location.href = logoutLink.href;
});
});
})();

View File

@ -1,89 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
header('Content-Type: application/json; charset=UTF-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'success' => false,
'message' => 'Méthode non autorisée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$submitted_cl_auth_user = trim((string) ($_POST['cl_auth_user'] ?? ''));
$submitted_cl_auth_pass = (string) ($_POST['cl_auth_pass'] ?? '');
if ($submitted_cl_auth_user === '' || $submitted_cl_auth_pass === '') {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'Identifiants incomplets.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmt_tbl_auth = db()->prepare(
'SELECT cl_auth_id, cl_auth_user, cl_auth_pass, cl_auth_right
FROM tbl_auth
WHERE cl_auth_user = :cl_auth_user
LIMIT 1'
);
$stmt_tbl_auth->execute([
'cl_auth_user' => $submitted_cl_auth_user,
]);
$tbl_auth = $stmt_tbl_auth->fetch();
if (!$tbl_auth) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Identifiants invalides.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$cl_auth_id = (int) $tbl_auth['cl_auth_id'];
$cl_auth_user = (string) $tbl_auth['cl_auth_user'];
$cl_auth_pass = (string) $tbl_auth['cl_auth_pass'];
$cl_auth_right = (string) $tbl_auth['cl_auth_right'];
unset($cl_auth_id);
if (!password_verify($submitted_cl_auth_pass, $cl_auth_pass)) {
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Identifiants invalides.',
], JSON_UNESCAPED_UNICODE);
exit;
}
if (password_needs_rehash($cl_auth_pass, PASSWORD_DEFAULT)) {
$rehash_cl_auth_pass = password_hash($submitted_cl_auth_pass, PASSWORD_DEFAULT);
$stmt_update_password = db()->prepare(
'UPDATE tbl_auth SET cl_auth_pass = :cl_auth_pass WHERE cl_auth_user = :cl_auth_user'
);
$stmt_update_password->execute([
'cl_auth_pass' => $rehash_cl_auth_pass,
'cl_auth_user' => $cl_auth_user,
]);
}
session_regenerate_id(true);
$_SESSION['user'] = $cl_auth_user;
$_SESSION['role'] = $cl_auth_right;
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
echo json_encode([
'success' => true,
'message' => 'Connexion réussie.',
'user' => $cl_auth_user,
'role' => $cl_auth_right,
'adminUrl' => 'admin.php',
'logoutUrl' => 'logout.php',
], JSON_UNESCAPED_UNICODE);

View File

@ -1,36 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$session_cookie_params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$session_cookie_params['path'],
$session_cookie_params['domain'],
$session_cookie_params['secure'],
$session_cookie_params['httponly']
);
}
session_destroy();
$is_ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
if ($is_ajax) {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
'success' => true,
'message' => 'Déconnexion effectuée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
header('Location: index.php');
exit;

View File

@ -1,590 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
require_once __DIR__ . '/db/scitemcustom.php';
require_once __DIR__ . '/db/sccharacters.php';
auth_start_session();
auth_bootstrap();
scstatsitem_bootstrap();
scitemcustom_bootstrap();
sccharacters_bootstrap();
$db = db();
$share_token = trim((string) ($_GET['share'] ?? ''));
$character = null;
$character_items = [];
$custom_stats_by_itemcustom = [];
$character_org_tag = '';
$character_has_player_handle = false;
if ($share_token !== '') {
$stmt_character = $db->prepare(
'SELECT c.*, COALESCE(NULLIF(TRIM(a.cl_auth_user), \'\'), \'Inconnu\') AS cl_sccharacter_creator_name
FROM tbl_sccharacters c
LEFT JOIN tbl_auth a ON a.cl_auth_id = c.cl_sccharacter_owner_auth_id
WHERE c.cl_sccharacter_share_token = :share_token
AND c.cl_sccharacter_share_enabled = 1
LIMIT 1'
);
$stmt_character->execute(['share_token' => $share_token]);
$character = $stmt_character->fetch() ?: null;
}
if ($character) {
$character_org_tag = sccharacters_resolve_org_tag($character);
$character_has_player_handle = sccharacters_has_player_handle($character);
sccharacters_reindex_character_items($db, (int) $character['cl_sccharacter_id']);
$stmt_items = $db->prepare(
"SELECT
ci.*,
bo.cl_scobjs_name AS cl_sccharacteritem_base_name,
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
bo.cl_scobjs_uuid AS cl_sccharacteritem_base_uuid,
oo.cl_scobjs_name AS cl_sccharacteritem_custom_name,
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype,
oo.cl_scobjs_uuid AS cl_sccharacteritem_custom_uuid
FROM tbl_sccharacteritems ci
LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
WHERE ci.cl_sccharacteritem_character_id = :character_id
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC"
);
$stmt_items->execute(['character_id' => (int) $character['cl_sccharacter_id']]);
$character_items = $stmt_items->fetchAll();
$custom_item_ids = [];
foreach ($character_items as $row) {
if (($row['cl_sccharacteritem_source'] ?? '') === 'custom' && !empty($row['cl_sccharacteritem_scitemcustom_id'])) {
$custom_item_ids[] = (int) $row['cl_sccharacteritem_scitemcustom_id'];
}
}
$custom_item_ids = array_values(array_unique(array_filter($custom_item_ids)));
if ($custom_item_ids !== []) {
$placeholders = implode(',', array_fill(0, count($custom_item_ids), '?'));
$stmt_stats = $db->prepare(
"SELECT
cs.cl_scitemcustomstat_itemcustom_id,
st.cl_scstatsitem_name,
st.cl_scstatsitem_unit,
cs.cl_scitemcustomstat_sign,
cs.cl_scitemcustomstat_value
FROM tbl_scitemcustomstat cs
INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
);
$stmt_stats->execute($custom_item_ids);
foreach ($stmt_stats->fetchAll() as $stat_row) {
$itemcustom_id = (int) $stat_row['cl_scitemcustomstat_itemcustom_id'];
if (!isset($custom_stats_by_itemcustom[$itemcustom_id])) {
$custom_stats_by_itemcustom[$itemcustom_id] = [];
}
$custom_stats_by_itemcustom[$itemcustom_id][] = $stat_row;
}
}
} else {
http_response_code(404);
}
$item_category_options = sccharacters_item_category_options();
$character_category_order = $character
? sccharacters_character_category_order($character)
: sccharacters_default_category_order();
$character_items_by_category = [];
foreach ($character_items as $item_row) {
$is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$type = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
$subtype = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
$category_key = sccharacters_resolve_item_category(
(string) ($item_row['cl_sccharacteritem_slot'] ?? ''),
$type,
$subtype
);
if (!isset($character_items_by_category[$category_key])) {
$character_items_by_category[$category_key] = [];
}
$character_items_by_category[$category_key][] = $item_row;
}
$character_items_by_category = sccharacters_sort_items_by_category_order(
$character_items_by_category,
$character_category_order
);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($character ? ((string) $character['cl_sccharacter_name'] . ' | Personnage partagé') : 'Personnage introuvable', ENT_QUOTES, 'UTF-8'); ?></title>
<style>
:root {
--primary: #a29b78;
--primary-soft: rgba(162, 155, 120, 0.18);
--primary-border: rgba(162, 155, 120, 0.3);
--bg: #080a0f;
--card: rgba(20, 24, 33, 0.88);
--text-main: #ece9df;
--text-soft: rgba(236, 233, 223, 0.72);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background-color: #050608;
color: var(--text-main);
font-family: Arial, Helvetica, sans-serif;
position: relative;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(5, 8, 12, 0.58), rgba(5, 8, 12, 0.72)),
url('https://robertsspaceindustries.com/media/1vllgn95062syr/background_blur/REACT-Background.jpg');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
z-index: -2;
transform: scale(1.08);
transform-origin: center center;
}
body::after {
content: '';
position: fixed;
inset: 0;
background: radial-gradient(circle at top right, rgba(29, 34, 52, 0.22) 0%, rgba(10, 13, 19, 0.18) 55%, rgba(5, 6, 8, 0.1) 100%);
z-index: -1;
pointer-events: none;
}
.page {
max-width: 1180px;
margin: 0 auto;
padding: 2rem 1rem 3rem;
position: relative;
z-index: 1;
}
.hero,
.card,
.message {
background: var(--card);
border: 1px solid var(--primary-border);
border-radius: 20px;
box-shadow: 0 22px 50px rgba(0,0,0,0.28);
backdrop-filter: blur(10px);
}
.hero {
padding: 1.4rem;
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
gap: 1.2rem;
margin-bottom: 1.3rem;
}
.avatar,
.avatar-fallback {
width: 120px;
height: 120px;
border-radius: 28px;
object-fit: cover;
background: linear-gradient(145deg, rgba(162, 155, 120, 0.3), rgba(255,255,255,0.08));
border: 1px solid rgba(255,255,255,0.08);
}
.avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
font-size: 2.2rem;
color: var(--primary);
font-weight: 700;
}
h1 { margin: 0 0 0.65rem; font-size: 2rem; }
p { line-height: 1.6; }
.meta,
.stats,
.tags {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
align-items: center;
}
.identity-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.7rem;
margin-bottom: 0.8rem;
}
.identity-item {
display: flex;
flex-direction: column;
gap: 0.24rem;
padding: 0.76rem 0.84rem;
border-radius: 14px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
}
.identity-label {
font-size: 0.7rem;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-soft);
}
.identity-value {
font-size: 0.96rem;
font-weight: 600;
color: #f6f7fb;
line-height: 1.35;
word-break: break-word;
}
.meta {
margin-top: 0.2rem;
}
.tag {
display: inline-flex;
align-items: center;
padding: 0.34rem 0.62rem;
border-radius: 999px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
font-size: 0.84rem;
}
.tag-primary {
background: var(--primary-soft);
border-color: rgba(162, 155, 120, 0.35);
color: #f6eebf;
}
.muted { color: var(--text-soft); }
.equipment-sections {
display: flex;
flex-direction: column;
gap: 2.2rem;
}
.equipment-section {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.equipment-section-head {
display: flex;
align-items: center;
gap: 0.8rem;
}
.equipment-section-title {
margin: 0;
color: var(--primary);
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.85rem;
width: 100%;
}
.equipment-section-title::after {
content: '';
flex: 1 1 auto;
height: 2px;
background: currentColor;
border-radius: 999px;
opacity: 0.95;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.card {
padding: 1rem;
display: flex;
gap: 0.95rem;
align-items: flex-start;
position: relative;
z-index: 0;
overflow: visible;
}
.card:hover,
.card:focus-within {
z-index: 2000;
}
.preview-container {
position: relative;
width: 68px;
height: 68px;
flex: 0 0 68px;
cursor: zoom-in;
}
.thumb,
.thumb-fallback {
width: 68px;
height: 68px;
border-radius: 18px;
object-fit: cover;
flex: 0 0 68px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
display: block;
}
.thumb-fallback {
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
font-size: 1.2rem;
}
.preview-floating {
visibility: hidden;
opacity: 0;
position: absolute;
top: -10px;
left: 82px;
z-index: 50;
padding: 5px;
background: var(--card);
border: 1px solid rgba(162, 155, 120, 0.4);
border-radius: 12px;
box-shadow: 0 14px 32px rgba(0,0,0,0.45);
backdrop-filter: blur(12px);
transition: opacity 0.2s ease, visibility 0.2s ease;
pointer-events: none;
}
.preview-floating img {
width: 260px;
height: 260px;
object-fit: contain;
display: block;
border-radius: 8px;
}
.preview-container:hover .preview-floating,
.preview-container:focus-within .preview-floating {
visibility: visible;
opacity: 1;
}
.card-body { min-width: 0; }
.card-body h3 { margin: 0 0 0.45rem; font-size: 1.05rem; }
.stats {
margin-top: 0.7rem;
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
align-items: stretch;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.stat {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(22, 78, 45, 0.14);
border: 1px solid rgba(90, 255, 150, 0.42);
box-shadow: none;
font-size: 0.68rem;
font-weight: 600;
line-height: 1.15;
color: #d7ffe6;
}
.section-title {
margin: 1.2rem 0 0.8rem;
color: var(--primary);
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 1rem;
}
.message {
padding: 1.4rem;
text-align: center;
}
@media (max-width: 860px) {
.hero,
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="page">
<?php if (!$character): ?>
<div class="message">
<h1>Personnage introuvable</h1>
<p class="muted">Ce lien public est invalide, désactivé ou nest plus disponible.</p>
</div>
<?php else: ?>
<?php
$avatar = trim((string) ($character['cl_sccharacter_avatar_url'] ?? ''));
$initial = function_exists('mb_substr')
? mb_strtoupper(mb_substr((string) $character['cl_sccharacter_name'], 0, 1, 'UTF-8'), 'UTF-8')
: strtoupper(substr((string) $character['cl_sccharacter_name'], 0, 1));
?>
<section class="hero">
<?php if ($avatar !== ''): ?>
<img class="avatar" src="<?php echo htmlspecialchars($avatar, ENT_QUOTES, 'UTF-8'); ?>" alt="Avatar de <?php echo htmlspecialchars((string) $character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className:'avatar-fallback', textContent:'<?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?>'}));">
<?php else: ?>
<div class="avatar-fallback"><?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<div>
<h1><?php echo htmlspecialchars((string) $character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h1>
<div class="identity-grid">
<div class="identity-item">
<span class="identity-label">Rôle / Classe</span>
<span class="identity-value"><?php echo htmlspecialchars(trim((string) $character['cl_sccharacter_role']) !== '' ? (string) $character['cl_sccharacter_role'] : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="identity-item">
<span class="identity-label">Tag</span>
<span class="identity-value"><?php echo htmlspecialchars($character_org_tag !== '' ? $character_org_tag : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="identity-item">
<span class="identity-label">Handle</span>
<span class="identity-value"><?php echo htmlspecialchars(trim((string) ($character['cl_sccharacter_player_handle'] ?? '')) !== '' ? (string) $character['cl_sccharacter_player_handle'] : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
</div>
<div class="meta">
<span class="tag muted">Créé par <?php echo htmlspecialchars((string) $character['cl_sccharacter_creator_name'], ENT_QUOTES, 'UTF-8'); ?></span>
<span class="tag muted"><?php echo count($character_items); ?> équipement(s)</span>
</div>
<p><?php echo nl2br(htmlspecialchars(trim((string) $character['cl_sccharacter_description']) !== '' ? (string) $character['cl_sccharacter_description'] : 'Aucune description publique fournie pour ce personnage.', ENT_QUOTES, 'UTF-8')); ?></p>
</div>
</section>
<?php if ($character_items === []): ?>
<div class="message">
<p class="muted">Ce personnage na pas encore déquipement attribué.</p>
</div>
<?php else: ?>
<div class="equipment-sections">
<?php foreach ($character_items_by_category as $category_key => $category_items): ?>
<section class="equipment-section">
<div class="equipment-section-head">
<h3 class="equipment-section-title"><?php echo htmlspecialchars(sccharacters_item_category_label((string) $category_key), ENT_QUOTES, 'UTF-8'); ?></h3>
</div>
<div class="grid">
<?php foreach ($category_items as $item_row): ?>
<?php
$is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$name = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_name'] ?? 'Objet personnalisé indisponible')
: (string) ($item_row['cl_sccharacteritem_base_name'] ?? 'Objet indisponible');
$type = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
$subtype = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
$uuid = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_uuid'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_uuid'] ?? '');
$category = sccharacters_resolve_item_category(
(string) ($item_row['cl_sccharacteritem_slot'] ?? ''),
$type,
$subtype
);
$quantity = isset($item_row['cl_sccharacteritem_quantity']) && (int) $item_row['cl_sccharacteritem_quantity'] > 0
? (int) $item_row['cl_sccharacteritem_quantity']
: null;
$title = $quantity !== null ? $quantity . 'x ' . $name : $name;
$note = trim((string) ($item_row['cl_sccharacteritem_note'] ?? ''));
$stats = $is_custom ? ($custom_stats_by_itemcustom[(int) ($item_row['cl_sccharacteritem_scitemcustom_id'] ?? 0)] ?? []) : [];
$image_url = $uuid !== '' ? 'https://cstone.space/uifimages/' . rawurlencode($uuid) . '.png' : '';
?>
<article class="card">
<?php if ($image_url !== ''): ?>
<div class="preview-container">
<img class="thumb" src="<?php echo htmlspecialchars($image_url, ENT_QUOTES, 'UTF-8'); ?>" alt="Aperçu de <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className:'thumb-fallback', textContent:'◈'}));">
<div class="preview-floating" aria-hidden="true">
<img src="<?php echo htmlspecialchars($image_url, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy" onerror="this.closest('.preview-floating').remove();">
</div>
</div>
<?php else: ?>
<div class="thumb-fallback"></div>
<?php endif; ?>
<div class="card-body">
<h3><?php echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></h3>
<div class="tags">
<span class="tag <?php echo $is_custom ? 'tag-primary' : ''; ?>"><?php echo $is_custom ? 'Objet perso.' : 'Base dobjets'; ?></span>
<span class="tag"><?php echo htmlspecialchars(sccharacters_item_category_label($category), ENT_QUOTES, 'UTF-8'); ?></span>
<?php if ($type !== ''): ?><span class="tag muted"><?php echo htmlspecialchars($type, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
<?php if ($subtype !== ''): ?><span class="tag muted"><?php echo htmlspecialchars($subtype, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
</div>
<?php if ($stats !== []): ?>
<div class="stats">
<?php foreach ($stats as $stat_row): ?>
<?php
$sign = (string) ($stat_row['cl_scitemcustomstat_sign'] ?? '');
$value = rtrim(rtrim(number_format((float) ($stat_row['cl_scitemcustomstat_value'] ?? 0), 2, '.', ''), '0'), '.');
if ($value === '') {
$value = '0';
}
?>
<span class="stat"><?php echo htmlspecialchars((string) $stat_row['cl_scstatsitem_name'] . ' : ' . $sign . $value . ' ' . (string) $stat_row['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($note !== ''): ?>
<p class="muted"><?php echo nl2br(htmlspecialchars($note, ENT_QUOTES, 'UTF-8')); ?></p>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,571 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scitems.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scitems.php', "Base d'Objets");
auth_require_page_access('scitems.php', "Base d'Objets");
scitems_bootstrap();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Pagination
$limit = 20;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
$offset = ($page - 1) * $limit;
// Search
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$where = "1=1";
$params = [];
if ($search !== '') {
$where = "(cl_scobjs_name LIKE :search OR cl_scobjs_type LIKE :search OR cl_scobjs_subtype LIKE :search OR cl_scobjs_uuid LIKE :search)";
$params['search'] = "%$search%";
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scitems.php');
exit;
}
$action = $_POST['action'] ?? '';
if ($action === 'import_json') {
if (!isset($_FILES['json_file']) || $_FILES['json_file']['error'] !== UPLOAD_ERR_OK) {
auth_flash_set('error', 'Erreur lors du téléchargement du fichier JSON.');
} else {
$jsonData = file_get_contents($_FILES['json_file']['tmp_name']);
$items = json_decode($jsonData, true);
if (!is_array($items)) {
auth_flash_set('error', 'Format JSON invalide (doit être un tableau).');
} else {
$count_new = 0;
$count_updated = 0;
$stmt_check = $db->prepare("SELECT cl_scobjs_id FROM tbl_scobjs WHERE cl_scobjs_uuid = :uuid");
$stmt_insert = $db->prepare("INSERT INTO tbl_scobjs (cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid, cl_scobjs_rarity, cl_scobjs_about, cl_scobjs_description) VALUES (:name, :type, :subtype, :uuid, '', '', :description)");
$stmt_update = $db->prepare("UPDATE tbl_scobjs SET cl_scobjs_name = :name, cl_scobjs_type = :type, cl_scobjs_subtype = :subtype, cl_scobjs_description = :description WHERE cl_scobjs_uuid = :uuid");
foreach ($items as $item) {
$uuid = trim((string) ($item['reference'] ?? ($item['stdItem']['UUID'] ?? '')));
if ($uuid === '') continue;
$name = trim((string) ($item['Name'] ?? ($item['stdItem']['Name'] ?? '')));
$classification = trim((string) ($item['classification'] ?? ''));
$parts = $classification !== '' ? explode('.', $classification) : [];
$type = trim((string) ($parts[1] ?? ($item['type'] ?? '')));
$subtype = trim((string) ($parts[2] ?? ($item['subType'] ?? '')));
$description = trim((string) ($item['Description'] ?? ($item['stdItem']['Description'] ?? '')));
$payload = [
'name' => $name,
'type' => $type,
'subtype' => $subtype,
'uuid' => $uuid,
'description' => $description !== '' ? $description : null,
];
$stmt_check->execute(['uuid' => $uuid]);
if ($stmt_check->fetch()) {
$stmt_update->execute($payload);
$count_updated++;
} else {
$stmt_insert->execute($payload);
$count_new++;
}
}
auth_flash_set('success', "Importation terminée : $count_new nouveaux, $count_updated mis à jour.");
}
}
header('Location: scitems.php');
exit;
}
if ($action === 'update_item') {
$id = (int)$_POST['id'];
$rarity = trim($_POST['rarity'] ?? '');
$about = trim($_POST['about'] ?? '');
$stmt = $db->prepare("UPDATE tbl_scobjs SET cl_scobjs_rarity = :rarity, cl_scobjs_about = :about WHERE cl_scobjs_id = :id");
$stmt->execute(['rarity' => $rarity, 'about' => $about, 'id' => $id]);
auth_flash_set('success', "Objet mis à jour avec succès.");
header('Location: scitems.php?page=' . $page . '&search=' . urlencode($search));
exit;
}
}
// Fetch items
$sql_count = "SELECT COUNT(*) FROM tbl_scobjs WHERE $where";
$stmt_count = $db->prepare($sql_count);
$stmt_count->execute($params);
$total_items = (int)$stmt_count->fetchColumn();
$total_pages = ceil($total_items / $limit);
$sql_items = "SELECT * FROM tbl_scobjs WHERE $where ORDER BY cl_scobjs_name ASC LIMIT $limit OFFSET $offset";
$stmt_items = $db->prepare($sql_items);
$stmt_items->execute($params);
$items_list = $stmt_items->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
$edit_id = isset($_GET['edit']) ? (int)$_GET['edit'] : 0;
$edit_item = null;
if ($edit_id > 0) {
$stmt_edit = $db->prepare("SELECT * FROM tbl_scobjs WHERE cl_scobjs_id = :id");
$stmt_edit->execute(['id' => $edit_id]);
$edit_item = $stmt_edit->fetch();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestion des Objets | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
/* Rarity Colors */
--rarity-L: #ff8000; /* Legendary - Orange */
--rarity-E: #a335ee; /* Epic - Purple */
--rarity-R: #0070dd; /* Rare - Blue */
--rarity-U: #1eff00; /* Uncommon - Green */
--rarity-C: #ffffff; /* Common - White */
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.topbar-actions {
display: flex;
gap: 1rem;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.admin-grid {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
select.form-control { background: #353b45; color: #fff; border-color: #565d68; color-scheme: dark; }
select.form-control:focus { background: #3d444f; color: #fff; }
select.form-control option { background: #353b45; color: #fff; }
select.form-control option:checked { background: #4a5260; color: #fff; }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); vertical-align: top; }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.pagination { display: flex; gap: 0.5rem; margin-top: 2rem; justify-content: center; }
.page-link {
padding: 0.5rem 1rem;
border: 1px solid var(--border-glow);
background: var(--card-bg);
color: #fff;
text-decoration: none;
border-radius: 4px;
transition: all 0.2s;
}
.page-link:hover, .page-link.active { background: var(--primary); color: var(--bg-dark); }
.search-container { margin-bottom: 1.5rem; display: flex; gap: 10px; }
.search-container input { flex: 1; }
.badge { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.7rem; text-transform: uppercase; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); }
.item-name { color: var(--primary); font-weight: bold; display: block; font-size: 1rem; margin-bottom: 4px; }
.item-meta { font-size: 0.75rem; color: #888; display: block; }
.item-uuid { font-size: 0.75rem; color: #777; font-family: monospace; word-break: break-all; display: block; margin-bottom: 4px; }
.item-about-cell { font-size: 0.85rem; color: #ccc; line-height: 1.4; }
/* Preview System */
.preview-container {
position: relative;
width: 80px;
height: 80px;
cursor: zoom-in;
}
.item-preview {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--border-glow);
background: rgba(0,0,0,0.5);
display: block;
}
.preview-floating {
visibility: hidden;
opacity: 0;
position: absolute;
top: -10px;
left: 95px;
z-index: 1000;
padding: 5px;
background: var(--card-bg);
border: 1px solid var(--primary);
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0,0,0,0.9), 0 0 20px var(--primary-glow);
backdrop-filter: blur(15px);
transition: opacity 0.3s ease, visibility 0.3s;
pointer-events: none;
}
.preview-floating img {
width: 350px;
height: 350px;
object-fit: contain;
display: block;
border-radius: 4px;
}
.preview-container:hover .preview-floating {
visibility: visible;
opacity: 1;
}
/* Rarity Classes */
.rarity-L { color: var(--rarity-L) !important; text-shadow: 0 0 10px rgba(255, 128, 0, 0.3); }
.rarity-E { color: var(--rarity-E) !important; text-shadow: 0 0 10px rgba(163, 53, 238, 0.3); }
.rarity-R { color: var(--rarity-R) !important; text-shadow: 0 0 10px rgba(0, 112, 221, 0.3); }
.rarity-U { color: var(--rarity-U) !important; text-shadow: 0 0 10px rgba(30, 255, 0, 0.3); }
.rarity-C { color: var(--rarity-C) !important; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scitems.php', "Base d'Objets"); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Objects Control</h1>
<p>Niveau d'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scitems.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<div class="side-panel">
<section class="glass-card" style="margin-bottom: 2rem;">
<h2>Importation JSON</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="import_json">
<div class="form-group">
<label for="json_file">Fichier JSON</label>
<input type="file" name="json_file" id="json_file" class="form-control" accept=".json" required>
</div>
<button type="submit" class="btn-modern" style="width: 100%;">Importer / Mettre à jour</button>
</form>
</section>
<?php if ($edit_item): ?>
<section class="glass-card">
<h2>Editer Objet</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="update_item">
<input type="hidden" name="id" value="<?php echo $edit_item['cl_scobjs_id']; ?>">
<div class="form-group">
<label>Nom</label>
<div class="form-control" style="background: rgba(255,255,255,0.05); border-color: transparent;">
<?php echo htmlspecialchars($edit_item['cl_scobjs_name']); ?>
</div>
</div>
<div class="form-group">
<label for="rarity">Rareté</label>
<select name="rarity" id="rarity" class="form-control">
<option value="" <?php echo $edit_item['cl_scobjs_rarity'] === '' ? 'selected' : ''; ?>>- Sélectionner -</option>
<option value="L" <?php echo $edit_item['cl_scobjs_rarity'] === 'L' ? 'selected' : ''; ?>>Legendary (L)</option>
<option value="E" <?php echo $edit_item['cl_scobjs_rarity'] === 'E' ? 'selected' : ''; ?>>Epic (E)</option>
<option value="R" <?php echo $edit_item['cl_scobjs_rarity'] === 'R' ? 'selected' : ''; ?>>Rare (R)</option>
<option value="U" <?php echo $edit_item['cl_scobjs_rarity'] === 'U' ? 'selected' : ''; ?>>Uncommon (U)</option>
<option value="C" <?php echo $edit_item['cl_scobjs_rarity'] === 'C' ? 'selected' : ''; ?>>Common (C)</option>
</select>
</div>
<div class="form-group">
<label for="about">Zone Admin / Infos</label>
<textarea name="about" id="about" class="form-control" rows="5" placeholder="Saisir les informations ici..."><?php echo htmlspecialchars($edit_item['cl_scobjs_about']); ?></textarea>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn-modern" style="flex: 2;">Sauvegarder</button>
<a href="scitems.php?page=<?php echo $page; ?>&search=<?php echo urlencode($search); ?>" class="btn-modern danger" style="flex: 1;">X</a>
</div>
</form>
</section>
<?php endif; ?>
</div>
<main class="main-panel">
<section class="glass-card">
<h2>
Base de Données d'Objets
<span style="font-size: 0.8rem; opacity: 0.6;"><?php echo $total_items; ?> entrées</span>
</h2>
<div class="search-container">
<form method="get" style="display: flex; width: 100%; gap: 10px;">
<input type="text" name="search" class="form-control" placeholder="Rechercher par nom, type, uuid..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="btn-modern">Filtrer</button>
<?php if ($search !== ''): ?>
<a href="scitems.php" class="btn-modern danger">Reset</a>
<?php endif; ?>
</form>
</div>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th style="width: 80px;">Aperçu</th>
<th style="width: 35%;">Nom / UUID / Type</th>
<th>About</th>
<th style="text-align: right; width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($items_list)): ?>
<tr><td colspan="4" style="text-align: center; padding: 3rem; color: #666;">Aucun objet trouvé.</td></tr>
<?php else: ?>
<?php foreach ($items_list as $item): ?>
<?php
$rarityClass = '';
if ($item['cl_scobjs_rarity']) {
$rarityClass = 'rarity-' . $item['cl_scobjs_rarity'];
}
$imageUrl = "https://cstone.space/uifimages/" . $item['cl_scobjs_uuid'] . ".png";
?>
<tr>
<td>
<div class="preview-container">
<img src="<?php echo $imageUrl; ?>" class="item-preview" alt="" loading="lazy">
<div class="preview-floating">
<img src="<?php echo $imageUrl; ?>" alt="">
</div>
</div>
</td>
<td>
<span class="item-name <?php echo $rarityClass; ?>"><?php echo htmlspecialchars($item['cl_scobjs_name']); ?></span>
<span class="item-uuid"><?php echo htmlspecialchars($item['cl_scobjs_uuid']); ?></span>
<span class="item-meta">
<?php echo htmlspecialchars($item['cl_scobjs_type']); ?>
<?php if($item['cl_scobjs_subtype']) echo " / " . htmlspecialchars($item['cl_scobjs_subtype']); ?>
</span>
</td>
<td class="item-about-cell">
<?php echo nl2br(htmlspecialchars($item['cl_scobjs_about'])); ?>
</td>
<td style="text-align: right;">
<a href="scitems.php?edit=<?php echo $item['cl_scobjs_id']; ?>&page=<?php echo $page; ?>&search=<?php echo urlencode($search); ?>" class="btn-modern btn-mini">Editer</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<?php if ($i == 1 || $i == $total_pages || ($i >= $page - 2 && $i <= $page + 2)): ?>
<a href="scitems.php?page=<?php echo $i; ?>&search=<?php echo urlencode($search); ?>" class="page-link <?php echo $i == $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php elseif ($i == $page - 3 || $i == $page + 3): ?>
<span style="padding: 0.5rem;">...</span>
<?php endif; ?>
<?php endfor; ?>
</div>
<?php endif; ?>
</section>
</main>
</div>
</div>
</body>
</html>

View File

@ -1,388 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scmanufactures.php', 'Manufactures');
auth_require_page_access('scmanufactures.php', 'Manufactures');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scmanufactures.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add manufacture
if ($action === 'add_manufacture') {
$name = trim($_POST['name'] ?? '');
if ($name !== '') {
try {
$stmt = $db->prepare("INSERT INTO tbl_scmanufactures (cl_scmanufactures_name) VALUES (:name)");
$stmt->execute(['name' => $name]);
auth_flash_set('success', 'Manufacture ajoutée avec succès.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Cette manufacture existe déjà.');
} else {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
}
} else {
auth_flash_set('error', 'Le nom de la manufacture est requis.');
}
header('Location: scmanufactures.php');
exit;
}
// Update manufacture
if ($action === 'update_manufacture') {
$id = (int)($_POST['manufacture_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
if ($id > 0 && $name !== '') {
try {
$stmt = $db->prepare("UPDATE tbl_scmanufactures SET cl_scmanufactures_name = :name WHERE cl_scmanufactures_id = :id");
$stmt->execute(['name' => $name, 'id' => $id]);
auth_flash_set('success', 'Manufacture mise à jour.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Données invalides.');
}
header('Location: scmanufactures.php');
exit;
}
// Delete manufacture
if ($action === 'delete_manufacture') {
$id = (int)($_POST['manufacture_id'] ?? 0);
if ($id > 0) {
try {
$stmt = $db->prepare("DELETE FROM tbl_scmanufactures WHERE cl_scmanufactures_id = :id");
$stmt->execute(['id' => $id]);
auth_flash_set('success', 'Manufacture supprimée.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la suppression. Assurez-vous qu\'aucun vaisseau n\'est lié à cette manufacture.');
}
}
header('Location: scmanufactures.php');
exit;
}
}
// Fetch all manufactures
$stmt_list = $db->query("SELECT * FROM tbl_scmanufactures ORDER BY cl_scmanufactures_name ASC");
$manufactures = $stmt_list->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manufactures | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scmanufactures.php', 'Manufactures'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>Gestion Manufactures</h1>
<p>Niveau d\'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scmanufactures.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Add/Edit -->
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouvelle Manufacture</h2>
<form id="manufactureForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" id="formAction" value="add_manufacture">
<input type="hidden" name="manufacture_id" id="manufactureId" value="">
<div class="form-group">
<label>Nom de la Manufacture</label>
<input type="text" name="name" id="manufactureName" class="form-control" required placeholder="ex: Roberts Space Industries">
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Manufactures</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($manufactures)): ?>
<tr><td colspan="3" style="text-align: center; padding: 3rem; color: #666;">Aucune manufacture enregistrée.</td></tr>
<?php else: ?>
<?php foreach ($manufactures as $m): ?>
<tr>
<td style="width: 50px; opacity: 0.5;">#<?php echo $m['cl_scmanufactures_id']; ?></td>
<td>
<strong style="color: var(--primary); text-transform: uppercase;"><?php echo htmlspecialchars($m['cl_scmanufactures_name']); ?></strong>
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="button" class="btn-modern btn-mini"
onclick='editManufacture(<?php echo json_encode([
"id" => $m["cl_scmanufactures_id"],
"name" => $m["cl_scmanufactures_name"]
]); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer cette manufacture ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_manufacture">
<input type="hidden" name="manufacture_id" value="<?php echo $m['cl_scmanufactures_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
function editManufacture(data) {
document.getElementById('formAction').value = 'update_manufacture';
document.getElementById('manufactureId').value = data.id;
document.getElementById('manufactureName').value = data.name;
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = 'Modifier Manufacture';
document.getElementById('manufactureForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_manufacture';
document.getElementById('manufactureId').value = '';
document.getElementById('manufactureForm').reset();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouvelle Manufacture';
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,322 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
require_once __DIR__ . '/db/scitemcustom.php';
require_once __DIR__ . '/db/scmanutention.php';
auth_start_session();
auth_bootstrap();
scstatsitem_bootstrap();
scitemcustom_bootstrap();
scmanutention_bootstrap();
function scmanutentionpublic_item_name(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_name'] ?? '')
: ($item_row['cl_scmanutentionitem_base_name'] ?? ''));
}
function scmanutentionpublic_item_type(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_type'] ?? '')
: ($item_row['cl_scmanutentionitem_base_type'] ?? ''));
}
function scmanutentionpublic_item_subtype(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_subtype'] ?? '')
: ($item_row['cl_scmanutentionitem_base_subtype'] ?? ''));
}
function scmanutentionpublic_item_uuid(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_uuid'] ?? '')
: ($item_row['cl_scmanutentionitem_base_uuid'] ?? ''));
}
function scmanutentionpublic_item_rarity(array $item_row): string
{
return (string) ((($item_row['cl_scmanutentionitem_source'] ?? '') === 'custom')
? ($item_row['cl_scmanutentionitem_custom_rarity'] ?? '')
: ($item_row['cl_scmanutentionitem_base_rarity'] ?? ''));
}
function scmanutentionpublic_rarity_class(string $rarity): string
{
$rarity = strtoupper(trim($rarity));
return in_array($rarity, ['L', 'E', 'R', 'U', 'C'], true) ? 'rarity-' . $rarity : '';
}
function scmanutentionpublic_stat_preview(array $stat_row): string
{
$sign = (string) ($stat_row['cl_scitemcustomstat_sign'] ?? '');
$prefix = $sign === '-' ? '-' : ($sign === '+' ? '+' : '');
$value = number_format((float) ($stat_row['cl_scitemcustomstat_value'] ?? 0), 2, '.', '');
$value = rtrim(rtrim($value, '0'), '.');
if ($value === '') {
$value = '0';
}
$unit = trim((string) ($stat_row['cl_scstatsitem_unit'] ?? ''));
return trim((string) ($stat_row['cl_scstatsitem_name'] ?? '') . ' : ' . $prefix . $value . ($unit !== '' ? ' ' . $unit : ''));
}
$db = db();
$share_token = trim((string) ($_GET['share'] ?? ''));
$sheet = scmanutention_find_public_sheet_by_token($db, $share_token);
$items = $sheet ? scmanutention_fetch_items($db, (int) $sheet['cl_scmanutention_id']) : [];
$custom_stats_map = scmanutention_fetch_custom_stats_map($db, $items);
if (!$sheet) {
http_response_code(404);
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($sheet ? ((string) $sheet['cl_scmanutention_title'] . ' | Fiche manutention') : 'Fiche introuvable', ENT_QUOTES, 'UTF-8'); ?></title>
<style>
:root {
--primary: #a29b78;
--primary-soft: rgba(162, 155, 120, 0.18);
--primary-border: rgba(162, 155, 120, 0.34);
--bg: #07090d;
--surface: rgba(15, 19, 28, 0.88);
--surface-soft: rgba(255,255,255,0.05);
--surface-border: rgba(255,255,255,0.08);
--text-main: #f2efe4;
--text-soft: rgba(242, 239, 228, 0.72);
--rarity-L: #ff8000;
--rarity-E: #a335ee;
--rarity-R: #0070dd;
--rarity-U: #1eff00;
--rarity-C: #ffffff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
linear-gradient(rgba(6, 8, 12, 0.8), rgba(6, 8, 12, 0.92)),
url('https://robertsspaceindustries.com/media/1vllgn95062syr/background_blur/REACT-Background.jpg') center/cover fixed;
color: var(--text-main);
font-family: Arial, Helvetica, sans-serif;
}
.page {
width: min(1180px, calc(100vw - 2rem));
margin: 0 auto;
padding: 2rem 0 3rem;
}
.hero,
.panel,
.item-card,
.message {
background: var(--surface);
border: 1px solid var(--primary-border);
border-radius: 22px;
box-shadow: 0 22px 46px rgba(0,0,0,0.32);
backdrop-filter: blur(12px);
}
.hero,
.panel,
.message {
padding: 1.2rem;
margin-bottom: 1rem;
}
h1, h2 { margin-top: 0; }
p { line-height: 1.6; }
.meta,
.item-meta,
.stats-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.34rem 0.62rem;
border-radius: 999px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-soft);
font-size: 0.82rem;
}
.items-list {
display: grid;
gap: 0.9rem;
}
.item-card {
padding: 0.95rem;
display: grid;
gap: 0.7rem;
}
.item-head {
display: flex;
gap: 0.85rem;
align-items: start;
}
.item-title-line {
display: flex;
align-items: baseline;
gap: 0.45rem;
flex-wrap: wrap;
}
.item-quantity-prefix {
font-weight: 800;
color: #fff2ca;
letter-spacing: 0.02em;
white-space: nowrap;
}
.item-preview {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 14px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
flex: none;
}
.item-text { min-width: 0; display: grid; gap: 5px; }
.item-name { font-size: 1.05rem; font-weight: 700; line-height: 1.2; }
.helper, .muted { color: var(--text-soft); }
.item-extra {
padding: 0.72rem 0.85rem;
border-radius: 14px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
}
.stat-pill {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0.38rem 0.72rem;
border-radius: 12px;
background: linear-gradient(135deg, rgba(26, 74, 42, 0.9), rgba(18, 46, 28, 0.82));
border: 1px solid rgba(90, 255, 150, 0.85);
box-shadow: inset 0 0 0 1px rgba(140, 255, 188, 0.18), 0 0 0 1px rgba(34, 110, 58, 0.28), 0 10px 22px rgba(0,0,0,0.18);
font-size: 0.8rem;
font-weight: 700;
color: #dcffe9;
}
.rarity-L { color: var(--rarity-L); text-shadow: 0 0 12px rgba(255, 128, 0, 0.28); }
.rarity-E { color: var(--rarity-E); text-shadow: 0 0 12px rgba(163, 53, 238, 0.28); }
.rarity-R { color: var(--rarity-R); text-shadow: 0 0 12px rgba(0, 112, 221, 0.28); }
.rarity-U { color: var(--rarity-U); text-shadow: 0 0 12px rgba(30, 255, 0, 0.28); }
.rarity-C { color: var(--rarity-C); }
@media (max-width: 700px) {
.page { width: min(100%, calc(100vw - 1rem)); }
.item-head { flex-wrap: wrap; }
}
</style>
</head>
<body>
<div class="page">
<?php if (!$sheet): ?>
<section class="message">
<h1>Fiche introuvable</h1>
<p>Cette fiche nexiste pas, nest plus publique, ou le lien partagé nest plus valide.</p>
</section>
<?php else: ?>
<section class="hero">
<h1><?php echo htmlspecialchars((string) $sheet['cl_scmanutention_title'], ENT_QUOTES, 'UTF-8'); ?></h1>
<div class="meta">
<?php if (!empty($sheet['cl_scmanutention_type'])): ?>
<span class="badge"><?php echo htmlspecialchars((string) $sheet['cl_scmanutention_type'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<?php if (!empty($sheet['cl_scmanutention_subtype'])): ?>
<span class="badge"><?php echo htmlspecialchars((string) $sheet['cl_scmanutention_subtype'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<span class="badge"><?php echo count($items); ?> objet(s)</span>
<span class="badge">Publié par <?php echo htmlspecialchars((string) ($sheet['cl_scmanutention_owner_name'] ?? 'Inconnu'), ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<?php if (!empty($sheet['cl_scmanutention_description'])): ?>
<p><?php echo nl2br(htmlspecialchars((string) $sheet['cl_scmanutention_description'], ENT_QUOTES, 'UTF-8')); ?></p>
<?php else: ?>
<p class="helper">Cette fiche publique ne contient pas encore de description.</p>
<?php endif; ?>
</section>
<section class="panel">
<h2>Contenu de la fiche</h2>
<?php if (empty($items)): ?>
<p class="muted">Aucun objet nest actuellement listé sur cette fiche.</p>
<?php else: ?>
<div class="items-list">
<?php foreach ($items as $item_row): ?>
<?php
$item_name = scmanutentionpublic_item_name($item_row);
$item_type = scmanutentionpublic_item_type($item_row);
$item_subtype = scmanutentionpublic_item_subtype($item_row);
$item_uuid = scmanutentionpublic_item_uuid($item_row);
$item_rarity_class = scmanutentionpublic_rarity_class(scmanutentionpublic_item_rarity($item_row));
$item_source = (string) ($item_row['cl_scmanutentionitem_source'] ?? 'base');
$item_custom_id = (int) ($item_row['cl_scmanutentionitem_scitemcustom_id'] ?? 0);
$item_stats = $item_source === 'custom' ? ($custom_stats_map[$item_custom_id] ?? []) : [];
?>
<article class="item-card">
<div class="item-head">
<?php if ($item_uuid !== ''): ?>
<img src="https://cstone.space/uifimages/<?php echo htmlspecialchars($item_uuid, ENT_QUOTES, 'UTF-8'); ?>.png" alt="" class="item-preview">
<?php endif; ?>
<div class="item-text">
<div class="item-title-line">
<span class="item-quantity-prefix"><?php echo (int) ($item_row['cl_scmanutentionitem_quantity'] ?? 1); ?>x</span>
<div class="item-name <?php echo htmlspecialchars($item_rarity_class, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($item_name, ENT_QUOTES, 'UTF-8'); ?></div>
</div>
<div class="item-meta">
<span class="badge"><?php echo htmlspecialchars($item_type !== '' ? $item_type : 'Sans type', ENT_QUOTES, 'UTF-8'); ?></span>
<?php if ($item_subtype !== ''): ?>
<span class="badge"><?php echo htmlspecialchars($item_subtype, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<span class="badge"><?php echo $item_source === 'custom' ? 'Objet perso' : 'Base d\'objets'; ?></span>
<?php if (!empty($item_stats)): ?>
<?php foreach ($item_stats as $stat_row): ?>
<span class="stat-pill"><?php echo htmlspecialchars(scmanutentionpublic_stat_preview($stat_row), ENT_QUOTES, 'UTF-8'); ?></span>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($item_row['cl_scmanutentionitem_extra_info'])): ?>
<div class="item-extra"><?php echo nl2br(htmlspecialchars((string) $item_row['cl_scmanutentionitem_extra_info'], ENT_QUOTES, 'UTF-8')); ?></div>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
</div>
</body>
</html>

View File

@ -1,642 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scmining.php', 'Scanner Minage');
auth_require_page_access('scmining.php', 'Scanner Minage');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scmining.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add mineral to list
if ($action === 'add_mineral') {
$obj_id = (int)$_POST['obj_id'];
$return_search = trim($_POST['return_search'] ?? '');
$return_page = max(1, (int)($_POST['return_page'] ?? 1));
if ($obj_id > 0) {
try {
$stmt = $db->prepare("INSERT INTO tbl_scmining (cl_scmining_obj_id, cl_scmining_scan_value, cl_scmining_max_occurrence) VALUES (:obj_id, 0, 1)");
$stmt->execute(['obj_id' => $obj_id]);
auth_flash_set('success', 'Minéral ajouté avec succès.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Ce minéral est déjà dans la liste.');
} else {
auth_flash_set('error', "Erreur lors de l'ajout : " . $e->getMessage());
}
}
}
header('Location: ' . scmining_search_url($return_search, $return_page));
exit;
}
// Update mineral values
if ($action === 'update_mineral') {
$mining_id = (int)$_POST['mining_id'];
$scan_value = (int)$_POST['scan_value'];
$max_occurrence = (int)$_POST['max_occurrence'];
$can_manual = isset($_POST['can_manual']) ? 1 : 0;
$can_land = isset($_POST['can_land']) ? 1 : 0;
$can_space = isset($_POST['can_space']) ? 1 : 0;
$stmt = $db->prepare("UPDATE tbl_scmining SET cl_scmining_scan_value = :scan, cl_scmining_max_occurrence = :occ, cl_scmining_can_manual = :manual, cl_scmining_can_land = :land, cl_scmining_can_space = :space WHERE cl_scmining_id = :id");
$stmt->execute([
'scan' => $scan_value,
'occ' => $max_occurrence,
'manual' => $can_manual,
'land' => $can_land,
'space' => $can_space,
'id' => $mining_id
]);
auth_flash_set('success', 'Valeurs mises à jour.');
header('Location: scmining.php');
exit;
}
// Remove mineral
if ($action === 'delete_mineral') {
$mining_id = (int)$_POST['mining_id'];
$stmt = $db->prepare("DELETE FROM tbl_scmining WHERE cl_scmining_id = :id");
$stmt->execute(['id' => $mining_id]);
auth_flash_set('success', 'Minéral retiré de la liste.');
header('Location: scmining.php');
exit;
}
}
// Search for adding items
function sc_normalize_rarity(?string $rarity): string
{
return strtoupper(trim((string) $rarity));
}
function sc_rarity_class(?string $rarity): string
{
$rarity = sc_normalize_rarity($rarity);
return in_array($rarity, ['L', 'E', 'R', 'U', 'C'], true) ? 'rarity-' . $rarity : 'rarity-none';
}
function sc_rarity_style(?string $rarity): string
{
return match (sc_normalize_rarity($rarity)) {
'L' => 'color:#ff8000 !important;text-shadow:0 0 10px rgba(255,128,0,0.3);',
'E' => 'color:#a335ee !important;text-shadow:0 0 10px rgba(163,53,238,0.3);',
'R' => 'color:#0070dd !important;text-shadow:0 0 10px rgba(0,112,221,0.3);',
'U' => 'color:#1eff00 !important;text-shadow:0 0 10px rgba(30,255,0,0.3);',
'C' => 'color:#ffffff !important;',
default => 'color:#8f96a3 !important;',
};
}
function scmining_search_url(string $search = '', int $page = 1): string
{
$params = [];
if ($search !== '') {
$params['search'] = $search;
}
if ($page > 1) {
$params['search_page'] = $page;
}
return 'scmining.php' . ($params ? '?' . http_build_query($params) : '');
}
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$search_page = max(1, (int)($_GET['search_page'] ?? 1));
$search_per_page = 10;
$search_results = [];
$search_total_results = 0;
$search_total_pages = 0;
$configured_matches = [];
if ($search !== '') {
$stmt_configured = $db->prepare("SELECT o.* FROM tbl_scobjs o JOIN tbl_scmining m ON m.cl_scmining_obj_id = o.cl_scobjs_id WHERE o.cl_scobjs_name LIKE :search ORDER BY o.cl_scobjs_name ASC, o.cl_scobjs_id ASC");
$stmt_configured->execute(['search' => "%$search%"]);
$configured_matches = $stmt_configured->fetchAll();
$search_where = "FROM tbl_scobjs o WHERE o.cl_scobjs_name LIKE :search AND o.cl_scobjs_id NOT IN (SELECT cl_scmining_obj_id FROM tbl_scmining)";
$stmt_search_count = $db->prepare("SELECT COUNT(*) " . $search_where);
$stmt_search_count->execute(['search' => "%$search%"]);
$search_total_results = (int)$stmt_search_count->fetchColumn();
$search_total_pages = max(1, (int)ceil($search_total_results / $search_per_page));
$search_page = min($search_page, $search_total_pages);
$search_offset = ($search_page - 1) * $search_per_page;
$stmt_search = $db->prepare("SELECT o.* " . $search_where . " ORDER BY o.cl_scobjs_name ASC, o.cl_scobjs_id ASC LIMIT :limit OFFSET :offset");
$stmt_search->bindValue(':search', "%$search%", PDO::PARAM_STR);
$stmt_search->bindValue(':limit', $search_per_page, PDO::PARAM_INT);
$stmt_search->bindValue(':offset', $search_offset, PDO::PARAM_INT);
$stmt_search->execute();
$search_results = $stmt_search->fetchAll();
}
// Fetch current mining list
$sql_list = "SELECT m.*, o.cl_scobjs_name, o.cl_scobjs_uuid, o.cl_scobjs_type, o.cl_scobjs_subtype, o.cl_scobjs_rarity
FROM tbl_scmining m
JOIN tbl_scobjs o ON m.cl_scmining_obj_id = o.cl_scobjs_id
ORDER BY o.cl_scobjs_name ASC";
$stmt_list = $db->query($sql_list);
$mining_list = $stmt_list->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scanner Minage | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--rarity-L: #ff8000;
--rarity-E: #a335ee;
--rarity-R: #0070dd;
--rarity-U: #1eff00;
--rarity-C: #ffffff;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.active {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.item-preview {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--border-glow);
background: rgba(0,0,0,0.5);
}
.search-result-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
background: rgba(255,255,255,0.05);
margin-bottom: 10px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
}
.search-result-info { flex: 1; }
.search-result-name { display: block; color: var(--primary); font-weight: bold; }
.search-result-uuid { display: block; font-size: 0.75rem; color: #777; font-family: monospace; word-break: break-all; margin-top: 2px; }
.search-result-meta { display: block; font-size: 0.75rem; color: #888; }
.search-results-section { margin-bottom: 1.25rem; }
.search-results-caption {
margin: 0 0 0.75rem;
font-size: 0.78rem;
color: #9a9a9a;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.search-result-item.is-configured {
border-color: rgba(162, 155, 120, 0.35);
background: rgba(162, 155, 120, 0.08);
}
.search-result-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.35rem 0.65rem;
border-radius: 999px;
border: 1px solid rgba(162, 155, 120, 0.35);
color: var(--primary);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
.search-results-summary {
margin-bottom: 1rem;
font-size: 0.8rem;
color: #9a9a9a;
}
.pagination-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 1rem;
}
.pagination-controls .btn-modern {
min-width: 42px;
}
.pagination-status {
font-size: 0.8rem;
color: #9a9a9a;
margin-left: auto;
}
.rarity-L { color: var(--rarity-L) !important; text-shadow: 0 0 10px rgba(255, 128, 0, 0.3); }
.rarity-E { color: var(--rarity-E) !important; text-shadow: 0 0 10px rgba(163, 53, 238, 0.3); }
.rarity-R { color: var(--rarity-R) !important; text-shadow: 0 0 10px rgba(0, 112, 221, 0.3); }
.rarity-U { color: var(--rarity-U) !important; text-shadow: 0 0 10px rgba(30, 255, 0, 0.3); }
.rarity-C { color: var(--rarity-C) !important; }
.rarity-none { color: #8f96a3 !important; }
.val-input { width: 100px; text-align: center; }
.recovery-options {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 0.75rem;
text-align: left;
}
.recovery-options label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
color: #ccc;
}
.recovery-options label:hover { color: var(--primary); }
.recovery-options input[type="checkbox"] {
accent-color: var(--primary);
cursor: pointer;
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scmining.php', 'Scanner Minage'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Mining Scanner</h1>
<p>Niveau d\'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scmining.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Search and Add -->
<div class="side-panel">
<section class="glass-card">
<h2>Ajouter un Minéral</h2>
<form method="get" action="scmining.php" style="display: flex; gap: 10px; margin-bottom: 1.5rem;">
<input type="text" name="search" class="form-control" placeholder="Rechercher (ex: Copper)" value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="btn-modern">OK</button>
</form>
<?php if ($search !== ''): ?>
<div class="search-results">
<?php if (!empty($configured_matches)): ?>
<div class="search-results-section">
<div class="search-results-caption">Déjà configuré<?php echo count($configured_matches) > 1 ? 's' : ''; ?> pour cette recherche</div>
<?php foreach ($configured_matches as $configured):
if (is_array($configured) && isset($configured['cl_scobjs_uuid']) && isset($configured['cl_scobjs_name']) && isset($configured['cl_scobjs_type']) && isset($configured['cl_scobjs_subtype'])) {
$configured_rarity_code = $configured['cl_scobjs_rarity'] ?? '';
$configured_rarity_class = sc_rarity_class($configured_rarity_code);
?>
<div class="search-result-item is-configured">
<img src="https://cstone.space/uifimages/<?php echo $configured['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
<div class="search-result-info">
<span class="search-result-name <?php echo htmlspecialchars($configured_rarity_class); ?>" style="<?php echo htmlspecialchars(sc_rarity_style($configured_rarity_code)); ?>"><?php echo htmlspecialchars($configured['cl_scobjs_name']); ?></span>
<span class="search-result-uuid">UUID: <?php echo htmlspecialchars($configured['cl_scobjs_uuid']); ?></span>
<span class="search-result-meta"><?php echo htmlspecialchars($configured['cl_scobjs_type']); ?> / <?php echo htmlspecialchars($configured['cl_scobjs_subtype']); ?> — déjà présent dans Configuration Minerais</span>
</div>
<span class="search-result-badge">Déjà configuré</span>
</div>
<?php } endforeach; ?>
</div>
<?php endif; ?>
<?php if (empty($search_results)): ?>
<p style="text-align: center; color: #666;">Aucun objet non listé trouvé.</p>
<?php else: ?>
<?php $search_first_result = (($search_page - 1) * $search_per_page) + 1; ?>
<?php $search_last_result = min($search_total_results, $search_first_result + count($search_results) - 1); ?>
<div class="search-results-summary">
Résultats <?php echo $search_first_result; ?> à <?php echo $search_last_result; ?> sur <?php echo $search_total_results; ?> — page <?php echo $search_page; ?>/<?php echo $search_total_pages; ?>
</div>
<?php foreach ($search_results as $res):
if (is_array($res) && isset($res['cl_scobjs_uuid']) && isset($res['cl_scobjs_name']) && isset($res['cl_scobjs_type']) && isset($res['cl_scobjs_subtype']) && isset($res['cl_scobjs_id'])) {
$rarity_code = $res['cl_scobjs_rarity'] ?? '';
$rarity_class = sc_rarity_class($rarity_code);
?>
<div class="search-result-item">
<img src="https://cstone.space/uifimages/<?php echo $res['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
<div class="search-result-info">
<span class="search-result-name <?php echo htmlspecialchars($rarity_class); ?>" style="<?php echo htmlspecialchars(sc_rarity_style($rarity_code)); ?>"><?php echo htmlspecialchars($res['cl_scobjs_name']); ?></span>
<span class="search-result-uuid">UUID: <?php echo htmlspecialchars($res['cl_scobjs_uuid']); ?></span>
<span class="search-result-meta"><?php echo htmlspecialchars($res['cl_scobjs_type']); ?> / <?php echo htmlspecialchars($res['cl_scobjs_subtype']); ?></span>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="add_mineral">
<input type="hidden" name="obj_id" value="<?php echo $res['cl_scobjs_id']; ?>">
<input type="hidden" name="return_search" value="<?php echo htmlspecialchars($search); ?>">
<input type="hidden" name="return_page" value="<?php echo $search_page; ?>">
<button type="submit" class="btn-modern btn-mini">+</button>
</form>
</div>
<?php } endforeach; ?>
<?php if ($search_total_pages > 1): ?>
<?php
$page_window_start = max(1, $search_page - 2);
$page_window_end = min($search_total_pages, $search_page + 2);
?>
<div class="pagination-controls">
<?php if ($search_page > 1): ?>
<a href="<?php echo htmlspecialchars(scmining_search_url($search, $search_page - 1)); ?>" class="btn-modern btn-mini">&laquo;</a>
<?php endif; ?>
<?php for ($page_number = $page_window_start; $page_number <= $page_window_end; $page_number++): ?>
<a href="<?php echo htmlspecialchars(scmining_search_url($search, $page_number)); ?>" class="btn-modern btn-mini<?php echo $page_number === $search_page ? ' active' : ''; ?>"><?php echo $page_number; ?></a>
<?php endfor; ?>
<?php if ($search_page < $search_total_pages): ?>
<a href="<?php echo htmlspecialchars(scmining_search_url($search, $search_page + 1)); ?>" class="btn-modern btn-mini">&raquo;</a>
<?php endif; ?>
<span class="pagination-status">Navigation des homonymes activée</span>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Configuration Minerais</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th style="width: 60px;">Aperçu</th>
<th>Nom / Type</th>
<th style="text-align: center;">Récupération</th>
<th style="text-align: center;">Valeur Scan</th>
<th style="text-align: center;">Max Occur.</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($mining_list)): ?>
<tr><td colspan="6" style="text-align: center; padding: 3rem; color: #666;">Aucun minéral configuré.</td></tr>
<?php else: ?>
<?php foreach ($mining_list as $item):
if (is_array($item) && isset($item['cl_scobjs_uuid']) && isset($item['cl_scobjs_name']) && isset($item['cl_scobjs_type']) && isset($item['cl_scobjs_subtype']) && isset($item['cl_scmining_id']) && isset($item['cl_scmining_scan_value']) && isset($item['cl_scmining_max_occurrence'])) {
$rarity_code = $item['cl_scobjs_rarity'] ?? '';
$rarity_class = sc_rarity_class($rarity_code);
?>
<tr>
<td>
<img src="https://cstone.space/uifimages/<?php echo $item['cl_scobjs_uuid']; ?>.png" class="item-preview" alt="">
</td>
<td>
<strong class="<?php echo htmlspecialchars($rarity_class); ?>" style="<?php echo htmlspecialchars(sc_rarity_style($rarity_code)); ?>"><?php echo htmlspecialchars($item['cl_scobjs_name']); ?></strong><br>
<span style="font-size: 0.75rem; color: #888;"><?php echo htmlspecialchars($item['cl_scobjs_type']); ?> / <?php echo htmlspecialchars($item['cl_scobjs_subtype']); ?></span>
</td>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="update_mineral">
<input type="hidden" name="mining_id" value="<?php echo $item['cl_scmining_id']; ?>">
<td style="text-align: center;">
<div class="recovery-options">
<label><input type="checkbox" name="can_manual" <?php echo ($item['cl_scmining_can_manual'] ?? 0) ? 'checked' : ''; ?>> Manuel</label>
<label><input type="checkbox" name="can_land" <?php echo ($item['cl_scmining_can_land'] ?? 0) ? 'checked' : ''; ?>> Terrestre</label>
<label><input type="checkbox" name="can_space" <?php echo ($item['cl_scmining_can_space'] ?? 0) ? 'checked' : ''; ?>> Spatial</label>
</div>
</td>
<td style="text-align: center;">
<input type="number" name="scan_value" class="form-control val-input" required value="<?php echo $item['cl_scmining_scan_value']; ?>">
</td>
<td style="text-align: center;">
<input type="number" name="max_occurrence" class="form-control val-input" required value="<?php echo $item['cl_scmining_max_occurrence']; ?>" min="1" max="10">
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="submit" class="btn-modern btn-mini">Save</button>
</form>
<form method="post" onsubmit="return confirm('Retirer ce minéral ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_mineral">
<input type="hidden" name="mining_id" value="<?php echo $item['cl_scmining_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php } endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
</body>
</html>

View File

@ -1,949 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scdiscord.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scnotification.php', 'NOTIF DISCORD');
auth_require_page_access('scnotification.php', 'NOTIF DISCORD');
scdiscord_bootstrap();
$db = db();
$csrf_token = auth_csrf_token();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$current_session_user = $_SESSION['user'] ?? '';
$current_session_role = $_SESSION['role'] ?? 'member';
$role_label = ($current_session_role === 'admin') ? 'Administrateur' : 'Membre';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = (string) ($_POST['csrf_token'] ?? '');
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scnotification.php');
exit;
}
$_SESSION['scnotification_old'] = $_POST;
$action = (string) ($_POST['action'] ?? '');
$is_legacy_send = $action === '' && (isset($_POST['description']) || isset($_POST['title']) || isset($_POST['webhook_choice']));
if ($action === 'send_notification' || $is_legacy_send) {
$legacy_webhook_choice = trim((string) ($_POST['webhook_choice'] ?? ''));
$cl_scnotification_webhook_id = (int) ($_POST['cl_scnotification_webhook_id'] ?? 0);
if ($cl_scnotification_webhook_id <= 0 && preg_match('/^notif(\d+)$/', $legacy_webhook_choice, $matches)) {
$cl_scnotification_webhook_id = (int) $matches[1];
}
$notify_here = isset($_POST['cl_scnotification_notify_here']) || isset($_POST['ping_here']);
$notify_everyone = isset($_POST['cl_scnotification_notify_everyone']) || isset($_POST['ping_everyone']);
$cl_scnotification_title = trim((string) ($_POST['cl_scnotification_title'] ?? ($_POST['title'] ?? '')));
$cl_scnotification_message = trim((string) ($_POST['cl_scnotification_message'] ?? ($_POST['description'] ?? '')));
$show_footer = isset($_POST['cl_scnotification_show_footer']);
$cl_scnotification_footer_text = trim((string) ($_POST['cl_scnotification_footer_text'] ?? ''));
$cl_scnotification_footer_icon_url = trim((string) ($_POST['cl_scnotification_footer_icon_url'] ?? ''));
$show_reactions = isset($_POST['cl_scnotification_show_reactions']) || isset($_POST['use_reactions']);
$show_thread = isset($_POST['cl_scnotification_show_thread']) || isset($_POST['use_publicthread']);
$show_org = isset($_POST['cl_scnotification_show_org']);
$cl_scnotification_org_value = trim((string) ($_POST['cl_scnotification_org_value'] ?? ''));
$show_pvp = isset($_POST['cl_scnotification_show_pvp']);
$cl_scnotification_pvp_value = trim((string) ($_POST['cl_scnotification_pvp_value'] ?? ''));
$include_schedule = isset($_POST['cl_scnotification_include_schedule']);
$cl_scnotification_location = trim((string) ($_POST['cl_scnotification_location'] ?? ''));
$cl_scnotification_start_date = trim((string) ($_POST['cl_scnotification_start_date'] ?? ''));
$cl_scnotification_departure_time = trim((string) ($_POST['cl_scnotification_departure_time'] ?? ''));
$include_briefing_time = isset($_POST['cl_scnotification_include_briefing_time']);
$cl_scnotification_briefing_time = trim((string) ($_POST['cl_scnotification_briefing_time'] ?? ''));
$include_end_date = isset($_POST['cl_scnotification_include_end_date']);
$cl_scnotification_end_date = trim((string) ($_POST['cl_scnotification_end_date'] ?? ''));
$include_end_time = isset($_POST['cl_scnotification_include_end_time']);
$cl_scnotification_end_time = trim((string) ($_POST['cl_scnotification_end_time'] ?? ''));
$show_channel_url = isset($_POST['cl_scnotification_show_channel_url']);
$cl_scnotification_channel_url = trim((string) ($_POST['cl_scnotification_channel_url'] ?? ''));
$show_inventory_url = isset($_POST['cl_scnotification_show_inventory_url']);
$cl_scnotification_inventory_url = trim((string) ($_POST['cl_scnotification_inventory_url'] ?? ''));
$show_source_url = isset($_POST['cl_scnotification_show_source_url']);
$cl_scnotification_source_url = trim((string) ($_POST['cl_scnotification_source_url'] ?? ''));
if ($cl_scnotification_webhook_id <= 0) {
auth_flash_set('error', 'Veuillez sélectionner un webhook Discord.');
header('Location: scnotification.php');
exit;
}
if ($cl_scnotification_message === '') {
auth_flash_set('error', 'Le champ description est obligatoire pour envoyer la notification.');
header('Location: scnotification.php');
exit;
}
$stmt_webhook = $db->prepare('SELECT * FROM tbl_scwebhooks WHERE cl_scwebhook_id = :id LIMIT 1');
$stmt_webhook->execute(['id' => $cl_scnotification_webhook_id]);
$webhook = $stmt_webhook->fetch();
if (!$webhook) {
auth_flash_set('error', 'Webhook Discord introuvable.');
header('Location: scnotification.php');
exit;
}
foreach ([
'URL canal Discord' => [$show_channel_url, $cl_scnotification_channel_url],
'URL inventaire' => [$show_inventory_url, $cl_scnotification_inventory_url],
'URL source' => [$show_source_url, $cl_scnotification_source_url],
'Icône du footer' => [$show_footer && $cl_scnotification_footer_icon_url !== '', $cl_scnotification_footer_icon_url],
] as $label => [$enabled, $value]) {
if ($enabled && $value !== '' && !filter_var($value, FILTER_VALIDATE_URL)) {
auth_flash_set('error', $label . ' invalide.');
header('Location: scnotification.php');
exit;
}
}
$mentions = scdiscord_build_mentions($notify_here, $notify_everyone);
$banner_image_url = (string) ($webhook['cl_scwebhook_image_url'] ?? '');
$border_color = (string) ($webhook['cl_scwebhook_border_color'] ?? '#ffae00');
$fields = [];
if ($show_org && $cl_scnotification_org_value !== '' && mb_strtolower($cl_scnotification_org_value) !== 'non') {
$fields[] = [
'name' => 'Tenue dorganisation',
'value' => mb_substr($cl_scnotification_org_value, 0, 1024),
'inline' => true,
];
}
if ($show_pvp && $cl_scnotification_pvp_value !== '' && mb_strtolower($cl_scnotification_pvp_value) !== 'inexistant') {
$fields[] = [
'name' => 'Risques PvP',
'value' => mb_substr($cl_scnotification_pvp_value, 0, 1024),
'inline' => true,
];
}
if ($include_schedule) {
if ($cl_scnotification_location !== '') {
$fields[] = ['name' => 'Lieu de ralliement', 'value' => mb_substr($cl_scnotification_location, 0, 1024), 'inline' => false];
}
if ($cl_scnotification_start_date !== '') {
$fields[] = ['name' => 'Date de début', 'value' => mb_substr($cl_scnotification_start_date, 0, 1024), 'inline' => true];
}
if ($cl_scnotification_departure_time !== '') {
$fields[] = ['name' => 'Heure de départ', 'value' => mb_substr($cl_scnotification_departure_time, 0, 1024), 'inline' => true];
}
if ($include_briefing_time && $cl_scnotification_briefing_time !== '') {
$fields[] = ['name' => 'Heure de briefing', 'value' => mb_substr($cl_scnotification_briefing_time, 0, 1024), 'inline' => true];
}
if ($include_end_date && $cl_scnotification_end_date !== '') {
$fields[] = ['name' => 'Date de fin', 'value' => mb_substr($cl_scnotification_end_date, 0, 1024), 'inline' => true];
}
if ($include_end_time && $cl_scnotification_end_time !== '') {
$fields[] = ['name' => 'Heure de fin', 'value' => mb_substr($cl_scnotification_end_time, 0, 1024), 'inline' => true];
}
}
if ($show_channel_url && $cl_scnotification_channel_url !== '') {
$fields[] = ['name' => 'Canal Discord', 'value' => '[Ouvrir le canal](' . $cl_scnotification_channel_url . ')', 'inline' => false];
}
if ($show_inventory_url && $cl_scnotification_inventory_url !== '') {
$fields[] = ['name' => 'Inventaire A.R.I.A', 'value' => '[Consulter linventaire](' . $cl_scnotification_inventory_url . ')', 'inline' => false];
}
if ($show_source_url && $cl_scnotification_source_url !== '') {
$fields[] = ['name' => 'Source', 'value' => '[Ouvrir la source](' . $cl_scnotification_source_url . ')', 'inline' => false];
}
$embed = [
'description' => mb_substr($cl_scnotification_message, 0, 4096),
'color' => scdiscord_hex_to_decimal($border_color),
];
if ($cl_scnotification_title !== '') {
$embed['title'] = mb_substr($cl_scnotification_title, 0, 256);
}
if (!empty($fields)) {
$embed['fields'] = $fields;
}
if ($banner_image_url !== '') {
$embed['image'] = ['url' => $banner_image_url];
}
if ($show_footer && ($cl_scnotification_footer_text !== '' || $cl_scnotification_footer_icon_url !== '')) {
$embed['footer'] = [];
if ($cl_scnotification_footer_text !== '') {
$embed['footer']['text'] = mb_substr($cl_scnotification_footer_text, 0, 2048);
}
if ($cl_scnotification_footer_icon_url !== '') {
$embed['footer']['icon_url'] = $cl_scnotification_footer_icon_url;
}
}
$payload = [
'embeds' => [$embed],
];
if (!empty($mentions)) {
$payload['content'] = implode(' ', $mentions);
$payload['allowed_mentions'] = ['parse' => ['everyone']];
}
$thread_name = $show_thread
? ('Discussion - ' . scdiscord_build_thread_name(
$cl_scnotification_title,
$cl_scnotification_location,
$cl_scnotification_start_date
))
: '';
$result = scdiscord_post_webhook((string) $webhook['cl_scwebhook_url'], $payload);
$bot_actions = [
'success' => true,
'http_code' => 200,
'response' => 'Aucune action bot demandée.',
];
if (!empty($result['success']) && ($show_reactions || $show_thread)) {
$message_data = scdiscord_decode_json_response((string) ($result['response'] ?? ''));
$bot_actions = scdiscord_apply_bot_actions($message_data, $show_reactions, $show_thread, $thread_name);
}
$log_response = json_encode([
'webhook' => [
'success' => !empty($result['success']),
'http_code' => (int) ($result['http_code'] ?? 0),
'response' => (string) ($result['response'] ?? ''),
],
'bot_actions' => [
'success' => !empty($bot_actions['success']),
'http_code' => (int) ($bot_actions['http_code'] ?? 0),
'response' => (string) ($bot_actions['response'] ?? ''),
'details' => $bot_actions['details'] ?? [],
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$stmt_log = $db->prepare(
'INSERT INTO tbl_scnotifications (
cl_scnotification_webhook_id,
cl_scnotification_title,
cl_scnotification_message,
cl_scnotification_payload,
cl_scnotification_response,
cl_scnotification_success,
cl_scnotification_created_by
) VALUES (
:webhook_id,
:title,
:message,
:payload,
:response,
:success,
:created_by
)'
);
$stmt_log->execute([
'webhook_id' => $cl_scnotification_webhook_id,
'title' => $cl_scnotification_title,
'message' => $cl_scnotification_message,
'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'response' => $log_response !== false ? $log_response : (string) ($result['response'] ?? ''),
'success' => (!empty($result['success']) && !empty($bot_actions['success'])) ? 1 : 0,
'created_by' => $current_session_user !== '' ? $current_session_user : 'Inconnu',
]);
if (empty($result['success'])) {
auth_flash_set('error', 'Échec de lenvoi Discord (HTTP ' . (int) ($result['http_code'] ?? 0) . ').');
} elseif (empty($bot_actions['success'])) {
auth_flash_set('error', 'Notification envoyée, mais échec des actions bot Discord : ' . (string) ($bot_actions['response'] ?? 'Erreur inconnue.'));
} else {
unset($_SESSION['scnotification_old']);
auth_flash_set('success', 'Notification Discord envoyée avec succès.');
}
header('Location: scnotification.php');
exit;
}
}
$old = $_SESSION['scnotification_old'] ?? [];
unset($_SESSION['scnotification_old']);
$stmt_webhooks = $db->query('SELECT * FROM tbl_scwebhooks ORDER BY cl_scwebhook_name ASC');
$webhooks = $stmt_webhooks->fetchAll();
function scnotification_old_value(array $old, string $key, string $default = ''): string
{
$value = $old[$key] ?? $default;
return is_string($value) ? $value : $default;
}
function scnotification_old_checked(array $old, string $key, bool $default = false): bool
{
if (empty($old)) {
return $default;
}
return array_key_exists($key, $old);
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SC Notification | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--text-soft: #d7d7d7;
--text-muted: #9ea3ad;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
overflow-x: hidden;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar,
.page-shell,
.history-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.6rem 1.2rem;
border-radius: 4px;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
text-decoration: none;
text-transform: uppercase;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'Electrolize', sans-serif;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger {
border-color: var(--danger);
color: var(--danger);
}
.btn-modern.danger:hover {
background: var(--danger);
color: #fff;
box-shadow: 0 0 15px rgba(255, 77, 77, 0.3);
}
.nav-tabs {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-glow);
}
.nav-tabs a {
text-decoration: none;
color: #888;
text-transform: uppercase;
font-size: 0.9rem;
transition: color 0.3s;
}
.nav-tabs a:hover,
.nav-tabs a.active {
color: var(--primary);
}
.flash {
margin-bottom: 1rem;
padding: 1rem 1.15rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.flash.success { color: #9ff3c8; border-color: rgba(0, 255, 136, 0.25); }
.flash.error { color: #ff9d9d; border-color: rgba(255, 77, 77, 0.25); }
.page-shell {
padding: 1.5rem;
}
.page-title {
margin: 0 0 1.5rem;
padding-bottom: 0.75rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-column {
display: grid;
gap: 1rem;
}
.section-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1.25rem;
}
.section-title {
margin: 0 0 1rem;
padding-bottom: 0.65rem;
color: var(--primary);
font-size: 1rem;
line-height: 1.2;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.field-row { margin-bottom: 0.85rem; }
.field-row:last-child { margin-bottom: 0; }
label, .inline-label {
display: block;
margin-bottom: 0.45rem;
font-size: 0.83rem;
color: #aaa;
text-transform: uppercase;
}
.check {
display: flex;
align-items: center;
gap: 0.55rem;
font-size: 0.9rem;
margin-bottom: 0.7rem;
color: #ededed;
text-transform: none;
}
.control,
textarea,
select,
input[type="text"],
input[type="url"],
input[type="date"],
input[type="time"] {
width: 100%;
padding: 0.8rem 1rem;
border-radius: 4px;
border: 1px solid #444;
background: rgba(0, 0, 0, 0.3);
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s, background 0.3s;
}
.control:focus,
textarea:focus,
select:focus,
input[type="text"]:focus,
input[type="url"]:focus,
input[type="date"]:focus,
input[type="time"]:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.5);
}
select.control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.control:focus {
background: #3d444f;
color: #fff;
}
select.control option {
background: #353b45;
color: #fff;
}
select.control option:checked {
background: #4a5260;
color: #fff;
}
textarea {
min-height: 180px;
resize: vertical;
}
input[disabled], select[disabled], textarea[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.subgrid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8rem;
}
.char-counter {
margin-top: 0.3rem;
font-size: 0.72rem;
color: var(--text-muted);
text-align: right;
}
.submit-wrap {
margin-top: 1rem;
}
.submit-wrap .btn-modern {
width: 100%;
padding: 0.95rem 1.2rem;
}
.history-card {
margin-top: 1.5rem;
padding: 1.2rem;
}
.history-card h2 {
margin: 0 0 1rem;
padding-bottom: 0.75rem;
color: var(--primary);
font-size: 1.1rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border-glow);
}
.history-list {
display: grid;
gap: 0.7rem;
}
.history-item {
padding: 0.85rem 0.95rem;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.history-meta {
margin-top: 0.25rem;
color: var(--text-muted);
font-size: 0.78rem;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
text-transform: uppercase;
}
.status-pill.ok {
background: rgba(0, 255, 136, 0.12);
color: #9ff3c8;
}
.status-pill.ko {
background: rgba(255, 77, 77, 0.12);
color: #ffb0b0;
}
.empty-state {
padding: 1.2rem;
border: 1px dashed rgba(255, 255, 255, 0.12);
border-radius: 10px;
text-align: center;
color: var(--text-muted);
}
@media (max-width: 1024px) {
.form-grid, .subgrid-2 { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scnotification.php', 'NOTIF DISCORD'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. SC Notification</h1>
<p>Niveau d'accès : <strong><?php echo htmlspecialchars($role_label, ENT_QUOTES, 'UTF-8'); ?></strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scnotification.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="page-shell">
<div class="page-title">Envoi d'une notification sur Discord</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="send_notification">
<div class="form-grid">
<div class="form-column">
<section class="section-card">
<h2 class="section-title">Canal de notification</h2>
<div class="field-row">
<select class="control" name="cl_scnotification_webhook_id" required>
<option value="">Sélectionner un webhook</option>
<?php foreach ($webhooks as $webhook): ?>
<?php $selected = (string) $webhook['cl_scwebhook_id'] === scnotification_old_value($old, 'cl_scnotification_webhook_id'); ?>
<option value="<?php echo (int) $webhook['cl_scwebhook_id']; ?>" <?php echo $selected ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($webhook['cl_scwebhook_name'] . (((int) $webhook['cl_scwebhook_is_forum'] === 1) ? ' [forum]' : ''), ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</section>
<section class="section-card">
<h2 class="section-title">Prévisualisation et Couleur</h2>
<p style="margin:0; color: var(--text-muted); line-height:1.6;">
Limage de prévisualisation et la couleur de bordure sont maintenant prises automatiquement depuis le canal Discord sélectionné.
Pour les modifier, va dans la page de configuration Discord du webhook correspondant.
</p>
</section>
<section class="section-card">
<h2 class="section-title">Message</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_notify_here" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_notify_here') ? 'checked' : ''; ?>>
<span>Notifier avec @here</span>
</label>
<label class="check">
<input type="checkbox" name="cl_scnotification_notify_everyone" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_notify_everyone', true) ? 'checked' : ''; ?>>
<span>Notifier avec @everyone</span>
</label>
<div class="field-row">
<label for="cl_scnotification_title">Titre :</label>
<input type="text" class="control" id="cl_scnotification_title" name="cl_scnotification_title" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_title'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-row">
<label for="cl_scnotification_message">Description :</label>
<textarea id="cl_scnotification_message" name="cl_scnotification_message" maxlength="2500" placeholder="Compatible avec le Markdown Discord..."><?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_message'), ENT_QUOTES, 'UTF-8'); ?></textarea>
<div class="char-counter"><span id="messageCount">2500</span> caractères restants</div>
</div>
</section>
<section class="section-card">
<h2 class="section-title">Options Footer</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_footer" id="cl_scnotification_show_footer" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_footer', true) ? 'checked' : ''; ?>>
<span>Afficher le footer</span>
</label>
<div class="field-row">
<label for="cl_scnotification_footer_text">Texte du footer :</label>
<input type="text" class="control footer-toggle" id="cl_scnotification_footer_text" name="cl_scnotification_footer_text" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_footer_text', 'R.E.A.C.T Initiative'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-row">
<label for="cl_scnotification_footer_icon_url">URL icône footer :</label>
<input type="url" class="control footer-toggle" id="cl_scnotification_footer_icon_url" name="cl_scnotification_footer_icon_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_footer_icon_url'), ENT_QUOTES, 'UTF-8'); ?>" placeholder="*.png">
</div>
</section>
<section class="section-card">
<h2 class="section-title">Réactions &amp; Fils</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_reactions" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_reactions') ? 'checked' : ''; ?>>
<span>Afficher les réactions 👍 / / / 👎</span>
</label>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_thread" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_thread', true) ? 'checked' : ''; ?>>
<span>Afficher le fil de discussion</span>
</label>
</section>
</div>
<div class="form-column">
<section class="section-card">
<h2 class="section-title">Tenue d'organisation &amp; PvP</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_org" id="cl_scnotification_show_org" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_org') ? 'checked' : ''; ?>>
<span>Afficher la tenue d'organisation</span>
</label>
<div class="field-row">
<select class="control org-toggle" name="cl_scnotification_org_value" id="cl_scnotification_org_value">
<?php foreach (['Non', 'Tenue civile', 'Tenue organisation', 'Tenue lourde'] as $option): ?>
<option value="<?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo scnotification_old_value($old, 'cl_scnotification_org_value', 'Non') === $option ? 'selected' : ''; ?>><?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_pvp" id="cl_scnotification_show_pvp" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_pvp') ? 'checked' : ''; ?>>
<span>Afficher les risques PvP</span>
</label>
<div class="field-row">
<select class="control pvp-toggle" name="cl_scnotification_pvp_value" id="cl_scnotification_pvp_value">
<?php foreach (['Inexistant', 'Faible', 'Modéré', 'Important', 'Extrême'] as $option): ?>
<option value="<?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo scnotification_old_value($old, 'cl_scnotification_pvp_value', 'Inexistant') === $option ? 'selected' : ''; ?>><?php echo htmlspecialchars($option, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
</section>
<section class="section-card">
<h2 class="section-title">Lieu, Date et Heure</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_schedule" id="cl_scnotification_include_schedule" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_schedule') ? 'checked' : ''; ?>>
<span>Inclure date, lieu et heures</span>
</label>
<div class="field-row">
<label for="cl_scnotification_location">Lieu de ralliement :</label>
<input type="text" class="control schedule-toggle" id="cl_scnotification_location" name="cl_scnotification_location" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_location'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-row subgrid-2">
<div>
<label for="cl_scnotification_start_date">Date de début :</label>
<input type="date" class="control schedule-toggle" id="cl_scnotification_start_date" name="cl_scnotification_start_date" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_start_date'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div>
<label for="cl_scnotification_departure_time">Heure de départ :</label>
<input type="time" class="control schedule-toggle" id="cl_scnotification_departure_time" name="cl_scnotification_departure_time" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_departure_time', '21:30'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_briefing_time" id="cl_scnotification_include_briefing_time" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_briefing_time') ? 'checked' : ''; ?>>
<span>Inclure une heure de briefing</span>
</label>
<div class="field-row">
<label for="cl_scnotification_briefing_time">Heure de briefing :</label>
<input type="time" class="control briefing-toggle" id="cl_scnotification_briefing_time" name="cl_scnotification_briefing_time" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_briefing_time', '21:00'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_end_date" id="cl_scnotification_include_end_date" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_end_date') ? 'checked' : ''; ?>>
<span>Inclure une date de fin</span>
</label>
<div class="field-row">
<label for="cl_scnotification_end_date">Date de fin :</label>
<input type="date" class="control enddate-toggle" id="cl_scnotification_end_date" name="cl_scnotification_end_date" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_end_date'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_include_end_time" id="cl_scnotification_include_end_time" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_include_end_time') ? 'checked' : ''; ?>>
<span>Inclure une heure de fin</span>
</label>
<div class="field-row">
<label for="cl_scnotification_end_time">Heure de fin :</label>
<input type="time" class="control endtime-toggle" id="cl_scnotification_end_time" name="cl_scnotification_end_time" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_end_time', '00:00'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
</section>
<section class="section-card">
<h2 class="section-title">URL externes</h2>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_channel_url" id="cl_scnotification_show_channel_url" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_channel_url') ? 'checked' : ''; ?>>
<span>Afficher un lien de canal Discord</span>
</label>
<div class="field-row">
<label for="cl_scnotification_channel_url">URL Canal :</label>
<input type="url" class="control channelurl-toggle" id="cl_scnotification_channel_url" name="cl_scnotification_channel_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_channel_url', 'https://discord.com/channels/...'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_inventory_url" id="cl_scnotification_show_inventory_url" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_inventory_url') ? 'checked' : ''; ?>>
<span>Afficher une URL de l'inventaire A.R.I.A</span>
</label>
<div class="field-row">
<label for="cl_scnotification_inventory_url">URL Inventaire :</label>
<input type="url" class="control inventoryurl-toggle" id="cl_scnotification_inventory_url" name="cl_scnotification_inventory_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_inventory_url', 'https://aria.blackops-agency.fr/...'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<label class="check">
<input type="checkbox" name="cl_scnotification_show_source_url" id="cl_scnotification_show_source_url" value="1" <?php echo scnotification_old_checked($old, 'cl_scnotification_show_source_url') ? 'checked' : ''; ?>>
<span>Afficher une URL source</span>
</label>
<div class="field-row">
<label for="cl_scnotification_source_url">URL Source :</label>
<input type="url" class="control sourceurl-toggle" id="cl_scnotification_source_url" name="cl_scnotification_source_url" value="<?php echo htmlspecialchars(scnotification_old_value($old, 'cl_scnotification_source_url', 'https://...'), ENT_QUOTES, 'UTF-8'); ?>">
</div>
</section>
</div>
</div>
<div class="submit-wrap">
<button type="submit" class="btn-modern">Envoyer</button>
</div>
</form>
</div>
</div>
<script>
const messageField = document.getElementById('cl_scnotification_message');
const messageCount = document.getElementById('messageCount');
function updateCounter() {
const remaining = 2500 - (messageField.value || '').length;
messageCount.textContent = remaining;
}
function toggleByCheckbox(checkboxId, selector, invert = false) {
const checkbox = document.getElementById(checkboxId);
const targets = document.querySelectorAll(selector);
if (!checkbox) return;
const enabled = invert ? !checkbox.checked : checkbox.checked;
targets.forEach((target) => {
target.disabled = !enabled;
});
}
function syncStates() {
toggleByCheckbox('cl_scnotification_show_footer', '.footer-toggle');
toggleByCheckbox('cl_scnotification_show_org', '.org-toggle');
toggleByCheckbox('cl_scnotification_show_pvp', '.pvp-toggle');
toggleByCheckbox('cl_scnotification_include_schedule', '.schedule-toggle');
const scheduleEnabled = document.getElementById('cl_scnotification_include_schedule')?.checked;
document.getElementById('cl_scnotification_include_briefing_time').disabled = !scheduleEnabled;
document.getElementById('cl_scnotification_include_end_date').disabled = !scheduleEnabled;
document.getElementById('cl_scnotification_include_end_time').disabled = !scheduleEnabled;
toggleByCheckbox('cl_scnotification_include_briefing_time', '.briefing-toggle');
toggleByCheckbox('cl_scnotification_include_end_date', '.enddate-toggle');
toggleByCheckbox('cl_scnotification_include_end_time', '.endtime-toggle');
if (!scheduleEnabled) {
document.querySelectorAll('.briefing-toggle, .enddate-toggle, .endtime-toggle').forEach(el => el.disabled = true);
}
toggleByCheckbox('cl_scnotification_show_channel_url', '.channelurl-toggle');
toggleByCheckbox('cl_scnotification_show_inventory_url', '.inventoryurl-toggle');
toggleByCheckbox('cl_scnotification_show_source_url', '.sourceurl-toggle');
}
updateCounter();
messageField.addEventListener('input', updateCounter);
[
'cl_scnotification_show_footer',
'cl_scnotification_show_org',
'cl_scnotification_show_pvp',
'cl_scnotification_include_schedule',
'cl_scnotification_include_briefing_time',
'cl_scnotification_include_end_date',
'cl_scnotification_include_end_time',
'cl_scnotification_show_channel_url',
'cl_scnotification_show_inventory_url',
'cl_scnotification_show_source_url'
].forEach((id) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', syncStates);
}
});
syncStates();
</script>
</body>
</html>

View File

@ -1,693 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scpreset.php', 'Presets Vaisseau');
auth_require_page_access('scpreset.php', 'Presets Vaisseau');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
$current_session_user = $_SESSION['user'] ?? '';
$current_session_role = $_SESSION['role'] ?? 'member';
$role_label = ($current_session_role === 'admin') ? 'Administrateur' : 'Membre';
function normalize_catalog_label(string $value): string {
$value = trim($value);
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
}
function find_preset_ship_relation(PDO $db, int $manufactureId, int $shipId): ?array {
if ($manufactureId <= 0 || $shipId <= 0) {
return null;
}
$stmt = $db->prepare(
"SELECT
m.cl_scmanufactures_id,
m.cl_scmanufactures_name,
v.cl_scvaisseaux_id,
v.cl_scvaisseaux_name
FROM tbl_scvaisseaux v
INNER JOIN tbl_scmanufactures m
ON m.cl_scmanufactures_id = v.cl_scvaisseaux_manufacture_id
WHERE m.cl_scmanufactures_id = :manufacture_id
AND v.cl_scvaisseaux_id = :ship_id
LIMIT 1"
);
$stmt->execute([
'manufacture_id' => $manufactureId,
'ship_id' => $shipId,
]);
$relation = $stmt->fetch();
return $relation ?: null;
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scpreset.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add preset
if ($action === 'add_preset') {
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
$ship_id = (int)($_POST['ship_id'] ?? 0);
$description = trim($_POST['description'] ?? '');
$link = trim($_POST['link'] ?? '');
$creator = $current_session_user ?: 'Inconnu';
$relation = find_preset_ship_relation($db, $manufacture_id, $ship_id);
if ($relation && $link !== '') {
try {
$stmt = $db->prepare("INSERT INTO tbl_scpreset (
cl_scpreset_manufacture_id,
cl_scpreset_vaisseau_id,
cl_scpreset_name,
cl_scpreset_manufacturer,
cl_scpreset_description,
cl_scpreset_link,
cl_scpreset_creator
) VALUES (
:manufacture_id,
:ship_id,
:name,
:manufacturer,
:description,
:link,
:creator
)");
$stmt->execute([
'manufacture_id' => $relation['cl_scmanufactures_id'],
'ship_id' => $relation['cl_scvaisseaux_id'],
'name' => $relation['cl_scvaisseaux_name'],
'manufacturer' => $relation['cl_scmanufactures_name'],
'description' => $description,
'link' => $link,
'creator' => $creator,
]);
auth_flash_set('success', 'Preset ajouté avec succès.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Veuillez sélectionner une manufacture, un vaisseau valide et renseigner le lien.');
}
header('Location: scpreset.php');
exit;
}
// Update preset
if ($action === 'update_preset') {
$preset_id = (int)($_POST['preset_id'] ?? 0);
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
$ship_id = (int)($_POST['ship_id'] ?? 0);
$description = trim($_POST['description'] ?? '');
$link = trim($_POST['link'] ?? '');
$relation = find_preset_ship_relation($db, $manufacture_id, $ship_id);
if ($preset_id > 0 && $relation && $link !== '') {
try {
$stmt = $db->prepare("UPDATE tbl_scpreset SET
cl_scpreset_manufacture_id = :manufacture_id,
cl_scpreset_vaisseau_id = :ship_id,
cl_scpreset_name = :name,
cl_scpreset_manufacturer = :manufacturer,
cl_scpreset_description = :description,
cl_scpreset_link = :link
WHERE cl_scpreset_id = :id");
$stmt->execute([
'manufacture_id' => $relation['cl_scmanufactures_id'],
'ship_id' => $relation['cl_scvaisseaux_id'],
'name' => $relation['cl_scvaisseaux_name'],
'manufacturer' => $relation['cl_scmanufactures_name'],
'description' => $description,
'link' => $link,
'id' => $preset_id,
]);
auth_flash_set('success', 'Preset mis à jour.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Données invalides : sélectionne une manufacture, un vaisseau valide et un lien.');
}
header('Location: scpreset.php');
exit;
}
// Delete preset
if ($action === 'delete_preset') {
$preset_id = (int)($_POST['preset_id'] ?? 0);
if ($preset_id > 0) {
$stmt = $db->prepare("DELETE FROM tbl_scpreset WHERE cl_scpreset_id = :id");
$stmt->execute(['id' => $preset_id]);
auth_flash_set('success', 'Preset supprimé.');
} else {
auth_flash_set('error', 'ID de preset invalide.');
}
header('Location: scpreset.php');
exit;
}
}
$stmt_mans = $db->query("SELECT * FROM tbl_scmanufactures ORDER BY cl_scmanufactures_name ASC");
$manufactures = $stmt_mans->fetchAll();
$stmt_ships = $db->query("SELECT
v.cl_scvaisseaux_id,
v.cl_scvaisseaux_name,
v.cl_scvaisseaux_manufacture_id,
m.cl_scmanufactures_name
FROM tbl_scvaisseaux v
INNER JOIN tbl_scmanufactures m ON m.cl_scmanufactures_id = v.cl_scvaisseaux_manufacture_id
ORDER BY m.cl_scmanufactures_name ASC, v.cl_scvaisseaux_name ASC");
$ships = $stmt_ships->fetchAll();
$manufacture_lookup = [];
$ships_by_manufacture = [];
$ships_by_id = [];
$ship_lookup = [];
foreach ($manufactures as $manufacture) {
$manufacture_lookup[normalize_catalog_label($manufacture['cl_scmanufactures_name'])] = (int)$manufacture['cl_scmanufactures_id'];
$ships_by_manufacture[(int)$manufacture['cl_scmanufactures_id']] = [];
}
foreach ($ships as $ship) {
$manufactureId = (int)$ship['cl_scvaisseaux_manufacture_id'];
$shipId = (int)$ship['cl_scvaisseaux_id'];
$shipName = $ship['cl_scvaisseaux_name'];
$manufacturerName = $ship['cl_scmanufactures_name'];
$ships_by_manufacture[$manufactureId][] = [
'id' => $shipId,
'name' => $shipName,
];
$ships_by_id[$shipId] = [
'id' => $shipId,
'name' => $shipName,
'manufacture_id' => $manufactureId,
'manufacturer_name' => $manufacturerName,
];
$ship_lookup[$manufactureId . '|' . normalize_catalog_label($shipName)] = $shipId;
}
$stmt_list = $db->query("SELECT
p.*,
m.cl_scmanufactures_name AS relation_manufacturer_name,
v.cl_scvaisseaux_name AS relation_ship_name,
v.cl_scvaisseaux_manufacture_id AS relation_ship_manufacture_id
FROM tbl_scpreset p
LEFT JOIN tbl_scmanufactures m ON m.cl_scmanufactures_id = p.cl_scpreset_manufacture_id
LEFT JOIN tbl_scvaisseaux v ON v.cl_scvaisseaux_id = p.cl_scpreset_vaisseau_id
ORDER BY COALESCE(m.cl_scmanufactures_name, p.cl_scpreset_manufacturer) ASC,
COALESCE(v.cl_scvaisseaux_name, p.cl_scpreset_name) ASC");
$presets = $stmt_list->fetchAll();
foreach ($presets as &$preset) {
$resolvedManufactureId = (int)($preset['cl_scpreset_manufacture_id'] ?? 0);
if ($resolvedManufactureId <= 0) {
$manufacturerKey = normalize_catalog_label((string)($preset['cl_scpreset_manufacturer'] ?? ''));
if ($manufacturerKey !== '' && isset($manufacture_lookup[$manufacturerKey])) {
$resolvedManufactureId = $manufacture_lookup[$manufacturerKey];
}
}
$resolvedShipId = (int)($preset['cl_scpreset_vaisseau_id'] ?? 0);
if ($resolvedShipId <= 0 && $resolvedManufactureId > 0) {
$shipKey = $resolvedManufactureId . '|' . normalize_catalog_label((string)($preset['cl_scpreset_name'] ?? ''));
if (isset($ship_lookup[$shipKey])) {
$resolvedShipId = $ship_lookup[$shipKey];
}
}
$displayManufacturer = $preset['relation_manufacturer_name'] ?: $preset['cl_scpreset_manufacturer'];
$displayName = $preset['relation_ship_name'] ?: $preset['cl_scpreset_name'];
if ($resolvedShipId > 0 && isset($ships_by_id[$resolvedShipId])) {
$displayName = $ships_by_id[$resolvedShipId]['name'];
$displayManufacturer = $ships_by_id[$resolvedShipId]['manufacturer_name'];
$resolvedManufactureId = $ships_by_id[$resolvedShipId]['manufacture_id'];
}
$preset['resolved_manufacture_id'] = $resolvedManufactureId;
$preset['resolved_ship_id'] = $resolvedShipId;
$preset['display_manufacturer'] = $displayManufacturer;
$preset['display_name'] = $displayName;
}
unset($preset);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Presets Vaisseaux | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
select.form-control { background: #353b45; color: #fff; border-color: #565d68; color-scheme: dark; }
select.form-control:focus { background: #3d444f; color: #fff; }
select.form-control option { background: #353b45; color: #fff; }
select.form-control option:checked { background: #4a5260; color: #fff; }
.form-control:disabled { opacity: 0.55; cursor: not-allowed; }
.form-help {
margin-top: 0.5rem;
font-size: 0.78rem;
color: #8f8f8f;
line-height: 1.45;
}
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.manufacturer-text {
font-size: 0.65rem;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
display: block;
margin-top: 2px;
}
.creator-text {
font-size: 0.65rem;
color: #888;
font-style: italic;
display: block;
margin-top: 1px;
}
.desc-text {
font-size: 0.8rem;
color: #aaa;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link-text {
font-size: 0.75rem;
color: var(--primary);
opacity: 0.8;
text-decoration: none;
}
.link-text:hover { text-decoration: underline; }
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scpreset.php', 'Presets Vaisseau'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Ship Presets</h1>
<p>Niveau d\'accès : <strong><?php echo htmlspecialchars($role_label); ?></strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scpreset.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Add/Edit Preset -->
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouveau Preset</h2>
<?php if (empty($manufactures)): ?>
<p style="color: var(--danger); font-size: 0.9rem;">Aucune manufacture n'est disponible. Ajoute d'abord une manufacture.</p>
<a href="scmanufactures.php" class="btn-modern" style="width: 100%;">Gérer les manufactures</a>
<?php elseif (empty($ships)): ?>
<p style="color: var(--danger); font-size: 0.9rem;">Aucun vaisseau n'est disponible. Ajoute d'abord au moins un vaisseau lié à une manufacture.</p>
<a href="scvaisseaux.php" class="btn-modern" style="width: 100%;">Gérer les vaisseaux</a>
<?php else: ?>
<form id="presetForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" id="formAction" value="add_preset">
<input type="hidden" name="preset_id" id="presetId" value="">
<div class="form-group">
<label>Manufacture</label>
<select name="manufacture_id" id="presetManufactureId" class="form-control" required>
<option value="">- Sélectionner une manufacture -</option>
<?php foreach ($manufactures as $m): ?>
<option value="<?php echo $m['cl_scmanufactures_id']; ?>"><?php echo htmlspecialchars($m['cl_scmanufactures_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Vaisseau</label>
<select name="ship_id" id="presetShipId" class="form-control" required disabled>
<option value="">- Choisissez d'abord une manufacture -</option>
</select>
<div class="form-help">Le vaisseau affiché dépend de la manufacture choisie. Le nom et la manufacture du preset seront remplis automatiquement.</div>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" id="presetDescription" class="form-control" rows="3" placeholder="Description du preset..."></textarea>
</div>
<div class="form-group">
<label>Lien Externe</label>
<input type="url" name="link" id="presetLink" class="form-control" required placeholder="https://...">
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
<?php endif; ?>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Presets</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>Vaisseau / Manufacture</th>
<th>Description</th>
<th>Lien</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($presets)): ?>
<tr><td colspan="4" style="text-align: center; padding: 3rem; color: #666;">Aucun preset enregistré.</td></tr>
<?php else: ?>
<?php foreach ($presets as $p): ?>
<tr>
<td>
<strong style="color: var(--primary); text-transform: uppercase;"><?php echo htmlspecialchars($p['display_name']); ?></strong><br>
<span class="manufacturer-text"><?php echo htmlspecialchars($p['display_manufacturer']); ?></span>
<span class="creator-text">Par <?php echo htmlspecialchars($p['cl_scpreset_creator'] ?: 'Inconnu'); ?></span>
</td>
<td>
<div class="desc-text" title="<?php echo htmlspecialchars($p['cl_scpreset_description']); ?>">
<?php echo htmlspecialchars($p['cl_scpreset_description'] ?: 'Aucune description'); ?>
</div>
</td>
<td>
<a href="<?php echo htmlspecialchars($p['cl_scpreset_link']); ?>" target="_blank" class="link-text">Consulter le lien</a>
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="button" class="btn-modern btn-mini"
onclick='editPreset(<?php echo json_encode([
"id" => $p["cl_scpreset_id"],
"manufacture_id" => $p["resolved_manufacture_id"],
"ship_id" => $p["resolved_ship_id"],
"name" => $p["display_name"],
"manufacturer" => $p["display_manufacturer"],
"description" => $p["cl_scpreset_description"],
"link" => $p["cl_scpreset_link"]
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer ce preset ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_preset">
<input type="hidden" name="preset_id" value="<?php echo $p['cl_scpreset_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
const shipsByManufacture = <?php echo json_encode($ships_by_manufacture, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
const presetForm = document.getElementById('presetForm');
const manufactureSelect = document.getElementById('presetManufactureId');
const shipSelect = document.getElementById('presetShipId');
function populateShipOptions(manufactureId, selectedShipId = '') {
if (!shipSelect) {
return;
}
shipSelect.innerHTML = '';
if (!manufactureId || !shipsByManufacture[manufactureId] || shipsByManufacture[manufactureId].length === 0) {
shipSelect.disabled = true;
shipSelect.innerHTML = '<option value="">- Choisissez d\'abord une manufacture -</option>';
return;
}
shipSelect.disabled = false;
shipSelect.innerHTML = '<option value="">- Sélectionner un vaisseau -</option>';
shipsByManufacture[manufactureId].forEach((ship) => {
const option = document.createElement('option');
option.value = String(ship.id);
option.textContent = ship.name;
if (selectedShipId && String(selectedShipId) === String(ship.id)) {
option.selected = true;
}
shipSelect.appendChild(option);
});
}
if (manufactureSelect) {
manufactureSelect.addEventListener('change', function () {
populateShipOptions(this.value, '');
});
populateShipOptions(manufactureSelect.value, shipSelect ? shipSelect.value : '');
}
function editPreset(data) {
if (!presetForm || !manufactureSelect || !shipSelect) {
return;
}
document.getElementById('formAction').value = 'update_preset';
document.getElementById('presetId').value = data.id;
manufactureSelect.value = data.manufacture_id ? String(data.manufacture_id) : '';
populateShipOptions(manufactureSelect.value, data.ship_id ? String(data.ship_id) : '');
document.getElementById('presetDescription').value = data.description;
document.getElementById('presetLink').value = data.link;
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = data.name && data.manufacturer
? `Modifier le Preset · ${data.manufacturer} / ${data.name}`
: 'Modifier le Preset';
presetForm.scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
if (!presetForm) {
return;
}
document.getElementById('formAction').value = 'add_preset';
document.getElementById('presetId').value = '';
presetForm.reset();
populateShipOptions('', '');
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouveau Preset';
}
</script>
</body>
</html>

View File

@ -1,517 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scstatsitem.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scstatsitem.php', 'Stats Item');
auth_require_page_access('scstatsitem.php', 'Stats Item');
scstatsitem_bootstrap();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
$allowed_units = ['', '%', '°C', 'RPM', 'Q', 'SCU', 'mRem', 'mRem/s'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scstatsitem.php');
exit;
}
$action = $_POST['action'] ?? '';
if ($action === 'add_stat') {
$name = trim($_POST['name'] ?? '');
$unit = trim($_POST['unit'] ?? '%');
if (!in_array($unit, $allowed_units, true)) {
$unit = '%';
}
if ($name === '') {
auth_flash_set('error', 'Le nom de la statistique est requis.');
} else {
try {
$stmt = $db->prepare('INSERT INTO tbl_scstatsitem (cl_scstatsitem_name, cl_scstatsitem_unit) VALUES (:name, :unit)');
$stmt->execute([
'name' => $name,
'unit' => $unit,
]);
auth_flash_set('success', 'Statistique ajoutée avec succès.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Cette statistique existe déjà.');
} else {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
}
}
header('Location: scstatsitem.php');
exit;
}
if ($action === 'update_stat') {
$id = (int) ($_POST['stat_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$unit = trim($_POST['unit'] ?? '%');
if (!in_array($unit, $allowed_units, true)) {
$unit = '%';
}
if ($id <= 0 || $name === '') {
auth_flash_set('error', 'Données invalides.');
} else {
try {
$stmt = $db->prepare('UPDATE tbl_scstatsitem SET cl_scstatsitem_name = :name, cl_scstatsitem_unit = :unit WHERE cl_scstatsitem_id = :id');
$stmt->execute([
'name' => $name,
'unit' => $unit,
'id' => $id,
]);
auth_flash_set('success', 'Statistique mise à jour.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
auth_flash_set('error', 'Cette statistique existe déjà.');
} else {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
}
}
header('Location: scstatsitem.php');
exit;
}
if ($action === 'delete_stat') {
$id = (int) ($_POST['stat_id'] ?? 0);
if ($id > 0) {
try {
$stmt = $db->prepare('DELETE FROM tbl_scstatsitem WHERE cl_scstatsitem_id = :id');
$stmt->execute(['id' => $id]);
auth_flash_set('success', 'Statistique supprimée.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la suppression : ' . $e->getMessage());
}
}
header('Location: scstatsitem.php');
exit;
}
}
$stmt_stats = $db->query('SELECT * FROM tbl_scstatsitem ORDER BY cl_scstatsitem_name ASC, cl_scstatsitem_id ASC');
$stats_items = $stmt_stats->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stats Item | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
.side-panel, .main-panel { display: flex; flex-direction: column; gap: 2rem; }
.glass-card {
background: var(--card-bg);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
}
.glass-card h2 {
margin: 0 0 1.25rem;
color: var(--primary);
font-size: 1.1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.form-group { margin-bottom: 1rem; }
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: var(--primary);
text-transform: uppercase;
letter-spacing: 1px;
}
.form-control {
width: 100%;
padding: 0.8rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-glow);
border-radius: 6px;
color: #fff;
font-family: 'Electrolize', sans-serif;
box-sizing: border-box;
transition: all 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-glow);
}
select.form-control {
background: #353b45;
color: #fff;
border-color: #565d68;
color-scheme: dark;
}
select.form-control:focus {
background: #3d444f;
color: #fff;
}
select.form-control option {
background: #353b45;
color: #fff;
}
select.form-control option:checked {
background: #4a5260;
color: #fff;
}
.form-help {
margin-top: 0.75rem;
color: #9ca3af;
font-size: 0.85rem;
line-height: 1.5;
}
.flash {
padding: 1rem 1.25rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border: 1px solid var(--border-glow);
background: rgba(20, 24, 33, 0.9);
}
.flash.success { border-color: rgba(0, 255, 136, 0.35); color: var(--success); }
.flash.error { border-color: rgba(255, 77, 77, 0.35); color: #ff8a8a; }
.modern-table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
}
.modern-table th,
.modern-table td {
padding: 0.9rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
vertical-align: middle;
}
.modern-table th {
text-align: left;
color: var(--primary);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.modern-table tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #777;
}
@media (max-width: 980px) {
.admin-grid {
grid-template-columns: 1fr;
}
.admin-topbar {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.nav-tabs {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scstatsitem.php', 'Stats Item'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>Stats Item</h1>
<p>Gestion libre des statistiques d'objets</p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scstatsitem.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouvelle Stat Item</h2>
<form id="statsItemForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" id="formAction" value="add_stat">
<input type="hidden" name="stat_id" id="statId" value="">
<div class="form-group">
<label for="statName">Nom de la statistique</label>
<input type="text" name="name" id="statName" class="form-control" required placeholder="Ex : Puissance, Résistance, Vitesse...">
<div class="form-help">Ajoute autant de stats que tu veux. Chaque ligne représente une statistique personnalisée que tu pourras gérer librement.</div>
</div>
<div class="form-group">
<label for="statUnit">Unité de la statistique</label>
<select name="unit" id="statUnit" class="form-control">
<?php foreach ($allowed_units as $unit_option): ?>
<option value="<?php echo htmlspecialchars($unit_option, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $unit_option === '%' ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($unit_option, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
</section>
</div>
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Stats Item</h2>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th>Unité</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($stats_items)): ?>
<tr>
<td colspan="4" class="empty-state">Aucune statistique enregistrée.</td>
</tr>
<?php else: ?>
<?php foreach ($stats_items as $stat): ?>
<tr>
<td style="width: 70px; opacity: 0.5;">#<?php echo (int) $stat['cl_scstatsitem_id']; ?></td>
<td>
<strong style="color: var(--primary); text-transform: uppercase;">
<?php echo htmlspecialchars($stat['cl_scstatsitem_name'], ENT_QUOTES, 'UTF-8'); ?>
</strong>
</td>
<td><?php echo htmlspecialchars($stat['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button
type="button"
class="btn-modern btn-mini"
onclick='editStatItem(<?php echo json_encode([
"id" => (int) $stat["cl_scstatsitem_id"],
"name" => $stat["cl_scstatsitem_name"],
"unit" => $stat["cl_scstatsitem_unit"],
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer cette statistique ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_stat">
<input type="hidden" name="stat_id" value="<?php echo (int) $stat['cl_scstatsitem_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
function editStatItem(data) {
document.getElementById('formAction').value = 'update_stat';
document.getElementById('statId').value = data.id;
document.getElementById('statName').value = data.name;
document.getElementById('statUnit').value = data.unit || '%';
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = 'Modifier Stat Item';
document.getElementById('statsItemForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_stat';
document.getElementById('statId').value = '';
document.getElementById('statsItemForm').reset();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouvelle Stat Item';
}
</script>
</body>
</html>

View File

@ -1,488 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scvaisseaux.php', 'Vaisseaux');
auth_require_page_access('scvaisseaux.php', 'Vaisseaux');
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = $_POST['csrf_token'] ?? '';
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scvaisseaux.php');
exit;
}
$action = $_POST['action'] ?? '';
// Add ship
if ($action === 'add_ship') {
$name = trim($_POST['name'] ?? '');
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
if ($name !== '' && $manufacture_id > 0) {
try {
$stmt = $db->prepare("INSERT INTO tbl_scvaisseaux (cl_scvaisseaux_name, cl_scvaisseaux_manufacture_id) VALUES (:name, :manufacture_id)");
$stmt->execute(['name' => $name, 'manufacture_id' => $manufacture_id]);
auth_flash_set('success', 'Vaisseau ajouté avec succès.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de l\'ajout : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Veuillez remplir tous les champs obligatoires.');
}
header('Location: scvaisseaux.php');
exit;
}
// Update ship
if ($action === 'update_ship') {
$id = (int)($_POST['ship_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$manufacture_id = (int)($_POST['manufacture_id'] ?? 0);
if ($id > 0 && $name !== '' && $manufacture_id > 0) {
try {
$stmt = $db->prepare("UPDATE tbl_scvaisseaux SET cl_scvaisseaux_name = :name, cl_scvaisseaux_manufacture_id = :manufacture_id WHERE cl_scvaisseaux_id = :id");
$stmt->execute(['name' => $name, 'manufacture_id' => $manufacture_id, 'id' => $id]);
auth_flash_set('success', 'Vaisseau mis à jour.');
} catch (PDOException $e) {
auth_flash_set('error', 'Erreur lors de la mise à jour : ' . $e->getMessage());
}
} else {
auth_flash_set('error', 'Données invalides.');
}
header('Location: scvaisseaux.php');
exit;
}
// Delete ship
if ($action === 'delete_ship') {
$id = (int)($_POST['ship_id'] ?? 0);
if ($id > 0) {
$stmt = $db->prepare("DELETE FROM tbl_scvaisseaux WHERE cl_scvaisseaux_id = :id");
$stmt->execute(['id' => $id]);
auth_flash_set('success', 'Vaisseau supprimé.');
}
header('Location: scvaisseaux.php');
exit;
}
}
// Fetch all manufactures for the dropdown
$stmt_mans = $db->query("SELECT * FROM tbl_scmanufactures ORDER BY cl_scmanufactures_name ASC");
$manufactures = $stmt_mans->fetchAll();
// Fetch all ships with manufacture names
$stmt_list = $db->query("SELECT v.*, m.cl_scmanufactures_name
FROM tbl_scvaisseaux v
JOIN tbl_scmanufactures m ON v.cl_scvaisseaux_manufacture_id = m.cl_scmanufactures_id
ORDER BY m.cl_scmanufactures_name ASC, v.cl_scvaisseaux_name ASC");
$ships = $stmt_list->fetchAll();
$current_session_user = $_SESSION['user'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vaisseaux | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
body {
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
margin: 0;
overflow-x: hidden;
min-height: 100vh;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: var(--card-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--border-glow);
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
padding: 0.6rem 1.2rem;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.9rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger { border-color: var(--danger); color: var(--danger); }
.btn-modern.danger:hover { background: var(--danger); color: #fff; }
.btn-mini { padding: 0.3rem 0.6rem; font-size: 0.75rem; }
.nav-tabs { display: flex; gap: 1rem; margin-bottom: 2rem; border-bottom: 1px solid var(--border-glow); padding-bottom: 1rem; }
.nav-tabs a { text-decoration: none; color: #888; text-transform: uppercase; font-size: 0.9rem; transition: color 0.3s; }
.nav-tabs a:hover, .nav-tabs a.active { color: var(--primary); }
.admin-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.admin-grid { grid-template-columns: 1fr; }
}
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
height: fit-content;
}
.glass-card h2 {
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
padding-bottom: 0.75rem;
}
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #aaa; text-transform: uppercase; }
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s;
}
.form-control:focus { outline: none; border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
select.form-control { background: #353b45; color: #fff; border-color: #565d68; color-scheme: dark; }
select.form-control:focus { background: #3d444f; color: #fff; }
select.form-control option { background: #353b45; color: #fff; }
select.form-control option:checked { background: #4a5260; color: #fff; }
.modern-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.modern-table th { text-align: left; padding: 1rem; font-size: 0.8rem; text-transform: uppercase; color: var(--primary); opacity: 0.7; }
.modern-table td { padding: 1rem; background: rgba(255, 255, 255, 0.03); border-top: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.modern-table td:first-child { border-left: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px 0 0 8px; }
.modern-table td:last-child { border-right: 1px solid rgba(255, 255, 255, 0.05); border-radius: 0 8px 8px 0; }
.modern-table tr:hover td { background: rgba(162, 155, 120, 0.05); }
.flash { padding: 1rem 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; font-size: 0.9rem; border-left: 4px solid var(--primary); background: rgba(162, 155, 120, 0.1); }
.flash.error { border-color: var(--danger); background: rgba(255, 77, 77, 0.1); color: #ffbaba; }
.flash.success { border-color: var(--success); background: rgba(0, 255, 136, 0.1); color: #baffda; }
.manufacturer-text {
font-size: 0.65rem;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
display: block;
margin-top: 2px;
}
.ship-filter-status {
margin: 0 0 1rem;
font-size: 0.9rem;
color: #b8bdc7;
}
.ship-filter-status strong {
color: #fff;
}
.ship-filter-empty {
display: none;
text-align: center;
padding: 1rem;
color: #8d94a0;
font-size: 0.95rem;
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scvaisseaux.php', 'Vaisseaux'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>Gestion Vaisseaux</h1>
<p>Niveau d\'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scvaisseaux.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type); ?>">
<?php echo htmlspecialchars($flash_message); ?>
</div>
<?php endif; ?>
<div class="admin-grid">
<!-- Left Column: Add/Edit -->
<div class="side-panel">
<section class="glass-card">
<h2 id="formTitle">Nouveau Vaisseau</h2>
<?php if (empty($manufactures)): ?>
<p style="color: var(--danger); font-size: 0.9rem;">Veuillez d\'abord ajouter au moins une manufacture.</p>
<a href="scmanufactures.php" class="btn-modern" style="width: 100%;">Aller aux Manufactures</a>
<?php else: ?>
<form id="shipForm" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" id="formAction" value="add_ship">
<input type="hidden" name="ship_id" id="shipId" value="">
<div class="form-group">
<label>Manufacture</label>
<select name="manufacture_id" id="shipManufacture" class="form-control" required>
<option value="">- Sélectionner -</option>
<?php foreach ($manufactures as $m): ?>
<option value="<?php echo $m['cl_scmanufactures_id']; ?>"><?php echo htmlspecialchars($m['cl_scmanufactures_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Nom du Vaisseau</label>
<input type="text" name="name" id="shipName" class="form-control" required placeholder="ex: Carrack">
</div>
<button type="submit" id="submitBtn" class="btn-modern" style="width: 100%;">Ajouter</button>
<button type="button" id="cancelBtn" class="btn-modern" style="width: 100%; margin-top: 10px; display: none;" onclick="resetForm()">Annuler</button>
</form>
<?php endif; ?>
</section>
</div>
<!-- Right Column: List -->
<main class="main-panel">
<section class="glass-card">
<h2>Liste des Vaisseaux</h2>
<p id="shipFilterStatus" class="ship-filter-status">Affichage : <strong>tous les vaisseaux</strong></p>
<div style="overflow-x: auto;">
<table class="modern-table">
<thead>
<tr>
<th>Manufacture / Modèle</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody id="shipTableBody">
<?php if (empty($ships)): ?>
<tr><td colspan="2" style="text-align: center; padding: 3rem; color: #666;">Aucun vaisseau enregistré.</td></tr>
<?php else: ?>
<?php foreach ($ships as $s): ?>
<tr class="ship-row" data-manufacture-id="<?php echo (int)$s['cl_scvaisseaux_manufacture_id']; ?>" data-manufacture-name="<?php echo htmlspecialchars($s['cl_scmanufactures_name'], ENT_QUOTES); ?>">
<td>
<span class="manufacturer-text"><?php echo htmlspecialchars($s['cl_scmanufactures_name']); ?></span>
<strong style="color: var(--primary); text-transform: uppercase; font-size: 1.1rem;"><?php echo htmlspecialchars($s['cl_scvaisseaux_name']); ?></strong>
</td>
<td style="text-align: right;">
<div style="display: flex; gap: 5px; justify-content: flex-end;">
<button type="button" class="btn-modern btn-mini"
onclick='editShip(<?php echo json_encode([
"id" => $s["cl_scvaisseaux_id"],
"name" => $s["cl_scvaisseaux_name"],
"manufacture_id" => $s["cl_scvaisseaux_manufacture_id"]
]); ?>)'>
Edit
</button>
<form method="post" onsubmit="return confirm('Supprimer ce vaisseau ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="action" value="delete_ship">
<input type="hidden" name="ship_id" value="<?php echo $s['cl_scvaisseaux_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">X</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php if (!empty($ships)): ?>
<p id="shipFilterEmpty" class="ship-filter-empty">Aucun vaisseau trouvé pour cette manufacture.</p>
<?php endif; ?>
</div>
</section>
</main>
</div>
</div>
<script>
function applyShipManufactureFilter() {
const manufactureSelect = document.getElementById('shipManufacture');
const status = document.getElementById('shipFilterStatus');
const emptyMessage = document.getElementById('shipFilterEmpty');
const rows = document.querySelectorAll('#shipTableBody .ship-row');
if (!manufactureSelect || !status || !rows.length) {
return;
}
const selectedManufactureId = manufactureSelect.value;
const selectedOption = manufactureSelect.options[manufactureSelect.selectedIndex];
const selectedManufactureName = selectedManufactureId && selectedOption
? selectedOption.text.trim()
: '';
let visibleCount = 0;
rows.forEach((row) => {
const matches = !selectedManufactureId || row.dataset.manufactureId === selectedManufactureId;
row.style.display = matches ? '' : 'none';
if (matches) {
visibleCount += 1;
}
});
if (selectedManufactureId && selectedManufactureName) {
const label = visibleCount > 1 ? 'vaisseaux' : 'vaisseau';
status.innerHTML = `Affichage : <strong>${visibleCount} ${label} pour ${selectedManufactureName}</strong>`;
} else {
const label = rows.length > 1 ? 'vaisseaux' : 'vaisseau';
status.innerHTML = `Affichage : <strong>tous les ${rows.length} ${label}</strong>`;
}
if (emptyMessage) {
emptyMessage.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
function editShip(data) {
document.getElementById('formAction').value = 'update_ship';
document.getElementById('shipId').value = data.id;
document.getElementById('shipName').value = data.name;
document.getElementById('shipManufacture').value = data.manufacture_id;
applyShipManufactureFilter();
document.getElementById('submitBtn').innerText = 'Mettre à jour';
document.getElementById('cancelBtn').style.display = 'block';
document.getElementById('formTitle').innerText = 'Modifier Vaisseau';
document.getElementById('shipForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add_ship';
document.getElementById('shipId').value = '';
document.getElementById('shipForm').reset();
applyShipManufactureFilter();
document.getElementById('submitBtn').innerText = 'Ajouter';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('formTitle').innerText = 'Nouveau Vaisseau';
}
document.addEventListener('DOMContentLoaded', function () {
const manufactureSelect = document.getElementById('shipManufacture');
if (!manufactureSelect) {
return;
}
manufactureSelect.addEventListener('change', applyShipManufactureFilter);
applyShipManufactureFilter();
});
</script>
</body>
</html>

View File

@ -1,641 +0,0 @@
<?php
require_once __DIR__ . '/db/auth.php';
require_once __DIR__ . '/db/scdiscord.php';
auth_start_session();
auth_bootstrap();
auth_handle_page_access_post('scwebhook.php', 'WEBHOOK');
auth_require_page_access('scwebhook.php', 'WEBHOOK');
scdiscord_bootstrap();
$db = db();
$csrf_token = auth_csrf_token();
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$current_session_user = $_SESSION['user'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted_csrf = (string) ($_POST['csrf_token'] ?? '');
if (!auth_validate_csrf($submitted_csrf)) {
auth_flash_set('error', 'Jeton CSRF invalide.');
header('Location: scwebhook.php');
exit;
}
$action = (string) ($_POST['action'] ?? '');
if ($action === 'add_webhook' || $action === 'update_webhook') {
$webhook_id = (int) ($_POST['webhook_id'] ?? 0);
$cl_scwebhook_name = trim((string) ($_POST['cl_scwebhook_name'] ?? ''));
$cl_scwebhook_url = trim((string) ($_POST['cl_scwebhook_url'] ?? ''));
$cl_scwebhook_image_url = trim((string) ($_POST['cl_scwebhook_image_url'] ?? ''));
$cl_scwebhook_border_color = scdiscord_normalize_hex_color((string) ($_POST['cl_scwebhook_border_color'] ?? ''));
$cl_scwebhook_is_forum = 0;
if ($cl_scwebhook_name === '' || $cl_scwebhook_url === '' || $cl_scwebhook_image_url === '') {
auth_flash_set('error', 'Le nom, lURL du webhook et limage sont obligatoires.');
header('Location: scwebhook.php');
exit;
}
if (!filter_var($cl_scwebhook_url, FILTER_VALIDATE_URL)) {
auth_flash_set('error', 'LURL du webhook Discord est invalide.');
header('Location: scwebhook.php');
exit;
}
if (!filter_var($cl_scwebhook_image_url, FILTER_VALIDATE_URL)) {
auth_flash_set('error', 'LURL de limage Discord est invalide.');
header('Location: scwebhook.php');
exit;
}
try {
if ($action === 'add_webhook') {
$stmt = $db->prepare('INSERT INTO tbl_scwebhooks (cl_scwebhook_name, cl_scwebhook_url, cl_scwebhook_image_url, cl_scwebhook_border_color, cl_scwebhook_is_forum) VALUES (:name, :url, :image_url, :border_color, :is_forum)');
$stmt->execute([
'name' => $cl_scwebhook_name,
'url' => $cl_scwebhook_url,
'image_url' => $cl_scwebhook_image_url,
'border_color' => $cl_scwebhook_border_color,
'is_forum' => $cl_scwebhook_is_forum,
]);
auth_flash_set('success', 'Canal Discord ajouté avec succès.');
} else {
if ($webhook_id <= 0) {
throw new RuntimeException('ID de webhook invalide.');
}
$stmt = $db->prepare('UPDATE tbl_scwebhooks SET cl_scwebhook_name = :name, cl_scwebhook_url = :url, cl_scwebhook_image_url = :image_url, cl_scwebhook_border_color = :border_color, cl_scwebhook_is_forum = :is_forum WHERE cl_scwebhook_id = :id');
$stmt->execute([
'name' => $cl_scwebhook_name,
'url' => $cl_scwebhook_url,
'image_url' => $cl_scwebhook_image_url,
'border_color' => $cl_scwebhook_border_color,
'is_forum' => $cl_scwebhook_is_forum,
'id' => $webhook_id,
]);
auth_flash_set('success', 'Canal Discord mis à jour.');
}
} catch (Throwable $e) {
auth_flash_set('error', 'Erreur webhook : ' . $e->getMessage());
}
header('Location: scwebhook.php');
exit;
}
if ($action === 'delete_webhook') {
$webhook_id = (int) ($_POST['webhook_id'] ?? 0);
if ($webhook_id > 0) {
try {
$stmt_usage = $db->prepare('SELECT COUNT(*) FROM tbl_scnotifications WHERE cl_scnotification_webhook_id = :id');
$stmt_usage->execute(['id' => $webhook_id]);
$usage_total = (int) $stmt_usage->fetchColumn();
if ($usage_total > 0) {
auth_flash_set('error', 'Suppression refusée : ce webhook est déjà relié à lhistorique des notifications.');
} else {
$stmt = $db->prepare('DELETE FROM tbl_scwebhooks WHERE cl_scwebhook_id = :id');
$stmt->execute(['id' => $webhook_id]);
auth_flash_set('success', 'Canal Discord supprimé.');
}
} catch (Throwable $e) {
auth_flash_set('error', 'Erreur suppression webhook : ' . $e->getMessage());
}
}
header('Location: scwebhook.php');
exit;
}
}
$stmt_webhooks = $db->query('SELECT * FROM tbl_scwebhooks ORDER BY cl_scwebhook_name ASC');
$webhooks = $stmt_webhooks->fetchAll();
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SC Discord | R.E.A.C.T. Admin</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/default.css">
<style>
:root {
--primary: #a29b78;
--primary-glow: rgba(162, 155, 120, 0.4);
--bg-dark: #080a0f;
--card-bg: rgba(20, 24, 33, 0.85);
--card-bg-secondary: rgba(255, 255, 255, 0.03);
--border-glow: rgba(162, 155, 120, 0.25);
--danger: #ff4d4d;
--success: #00ff88;
--muted: #aaaaaa;
}
@font-face {
font-family: 'Electrolize';
src: url('fonts/Electrolize-Regular.ttf') format('truetype');
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top right, #1a1f2e, var(--bg-dark));
background-attachment: fixed;
color: #e0e0e0;
font-family: 'Electrolize', sans-serif;
overflow-x: hidden;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.admin-topbar,
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--border-glow);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.admin-topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
}
.topbar-info h1 {
margin: 0;
font-size: 1.5rem;
letter-spacing: 2px;
text-transform: uppercase;
background: linear-gradient(90deg, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.topbar-info p {
margin: 0.25rem 0 0;
font-size: 0.85rem;
color: var(--primary);
opacity: 0.8;
}
.topbar-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.session-user {
opacity: 0.85;
}
.btn-modern {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
padding: 0.6rem 1.2rem;
border-radius: 4px;
border: 1px solid var(--primary);
background: transparent;
color: #fff;
text-decoration: none;
text-transform: uppercase;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'Electrolize', sans-serif;
}
.btn-modern:hover {
background: var(--primary);
color: var(--bg-dark);
box-shadow: 0 0 15px var(--primary-glow);
}
.btn-modern.danger {
border-color: var(--danger);
color: var(--danger);
}
.btn-modern.danger:hover {
background: var(--danger);
color: #fff;
box-shadow: 0 0 15px rgba(255, 77, 77, 0.3);
}
.btn-modern.secondary {
border-color: rgba(255, 255, 255, 0.2);
color: #d6d6d6;
}
.btn-modern.secondary:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
box-shadow: none;
}
.btn-mini {
padding: 0.45rem 0.8rem;
font-size: 0.72rem;
}
.nav-tabs {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-glow);
}
.nav-tabs a {
text-decoration: none;
color: #888;
text-transform: uppercase;
font-size: 0.9rem;
transition: color 0.3s;
}
.nav-tabs a:hover,
.nav-tabs a.active {
color: var(--primary);
}
.flash {
margin-bottom: 1.5rem;
padding: 1rem 1.2rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.flash.success { border-color: rgba(0, 255, 136, 0.25); color: #9ff3c8; }
.flash.error { border-color: rgba(255, 77, 77, 0.25); color: #ff9d9d; }
.page-grid {
display: grid;
grid-template-columns: 420px 1fr;
gap: 2rem;
}
.stack {
display: grid;
gap: 1.5rem;
}
.glass-card {
padding: 1.5rem;
}
.glass-card h2 {
margin: 0 0 1.5rem;
padding-bottom: 0.75rem;
font-size: 1.25rem;
color: var(--primary);
border-bottom: 1px solid var(--border-glow);
}
.section-note {
margin: -0.5rem 0 1.2rem;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
}
.form-grid {
display: grid;
gap: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #aaa;
text-transform: uppercase;
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid #444;
border-radius: 4px;
color: #fff;
font-family: 'Electrolize', sans-serif;
transition: border-color 0.3s, background 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
background: rgba(0, 0, 0, 0.5);
}
.form-control[type="color"] {
min-height: 48px;
padding: 0.35rem;
}
.checkbox-row {
display: flex;
align-items: center;
gap: 0.7rem;
color: #e7e7e7;
font-size: 0.9rem;
}
.list-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
}
th {
text-align: left;
padding: 1rem 0.75rem;
font-size: 0.8rem;
text-transform: uppercase;
color: var(--primary);
opacity: 0.7;
}
td {
padding: 1rem 0.75rem;
background: var(--card-bg-secondary);
border-top: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
vertical-align: top;
text-align: left;
font-size: 0.88rem;
}
td:first-child {
border-left: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px 0 0 8px;
}
td:last-child {
border-right: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 0 8px 8px 0;
}
tr:hover td {
background: rgba(162, 155, 120, 0.08);
}
.muted {
color: var(--muted);
font-size: 0.8rem;
}
.pill {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.07);
font-size: 0.72rem;
color: #fff;
}
.pill.forum {
background: rgba(162, 155, 120, 0.18);
color: #e5dcb7;
}
.banner-preview {
width: 120px;
max-height: 42px;
object-fit: cover;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
display: block;
background: #17191e;
}
.color-chip {
display: inline-flex;
align-items: center;
gap: 0.45rem;
}
.color-chip::before {
content: '';
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--chip-color, var(--primary));
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
}
.actions-inline {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: flex-end;
}
.empty-state {
padding: 2rem;
border: 1px dashed rgba(255, 255, 255, 0.12);
border-radius: 10px;
color: #9a9a9a;
text-align: center;
}
@media (max-width: 1100px) {
.page-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<?php echo auth_render_page_access_widget('scwebhook.php', 'WEBHOOK'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. SC Webhook</h1>
<p>Niveau d'accès : <strong>Administrateur</strong></p>
</div>
<div class="topbar-actions">
<span class="session-user">Connecté : <strong><?php echo htmlspecialchars($current_session_user, ENT_QUOTES, 'UTF-8'); ?></strong></span>
<a href="index.php" class="btn-modern">Retour au site</a>
<a href="logout.php" class="btn-modern danger">Déconnexion</a>
</div>
</header>
<?php echo auth_render_app_nav('scwebhook.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo htmlspecialchars($flash_type, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="page-grid">
<aside class="stack">
<section class="glass-card">
<h2 id="webhookFormTitle">Nouveau canal Discord</h2>
<p class="section-note">Configure ici toute la logique centralisée du canal Discord : nomination, URL du webhook, image de prévisualisation et couleur de bordure.</p>
<form method="post" id="webhookForm" class="form-grid">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" id="webhookAction" value="add_webhook">
<input type="hidden" name="webhook_id" id="webhookId" value="">
<div class="form-group">
<label for="cl_scwebhook_name">Nomination du canal</label>
<input type="text" id="cl_scwebhook_name" name="cl_scwebhook_name" class="form-control" required placeholder="Ex : Code 4">
</div>
<div class="form-group">
<label for="cl_scwebhook_url">Lien du webhook</label>
<input type="url" id="cl_scwebhook_url" name="cl_scwebhook_url" class="form-control" required placeholder="https://discord.com/api/webhooks/...">
</div>
<div class="form-group">
<label for="cl_scwebhook_image_url">Lien image / prévisualisation</label>
<input type="url" id="cl_scwebhook_image_url" name="cl_scwebhook_image_url" class="form-control" required placeholder="https://.../banniere.png">
</div>
<div class="form-group">
<label for="cl_scwebhook_border_color">Couleur de bordure</label>
<input type="color" id="cl_scwebhook_border_color" name="cl_scwebhook_border_color" class="form-control" value="#ffae00">
</div>
<button type="submit" class="btn-modern" id="webhookSubmitBtn">Ajouter le canal</button>
<button type="button" class="btn-modern secondary" id="webhookCancelBtn" style="display:none;" onclick="resetWebhookForm()">Annuler lédition</button>
</form>
</section>
</aside>
<main class="list-grid">
<section class="glass-card">
<h2>Canaux Discord enregistrés</h2>
<?php if (empty($webhooks)): ?>
<div class="empty-state">Aucun canal Discord configuré pour le moment.</div>
<?php else: ?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Nomination</th>
<th>Webhook</th>
<th>Prévisualisation</th>
<th>Couleur</th>
<th style="text-align:right;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($webhooks as $webhook): ?>
<tr>
<td>
<strong><?php echo htmlspecialchars($webhook['cl_scwebhook_name'], ENT_QUOTES, 'UTF-8'); ?></strong>
</td>
<td>
<span class="muted"><?php echo htmlspecialchars(scdiscord_mask_webhook_url((string) $webhook['cl_scwebhook_url']), ENT_QUOTES, 'UTF-8'); ?></span>
</td>
<td>
<img class="banner-preview" src="<?php echo htmlspecialchars((string) ($webhook['cl_scwebhook_image_url'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>" alt="Prévisualisation webhook">
</td>
<td>
<?php $webhookBorderColor = scdiscord_normalize_hex_color((string) ($webhook['cl_scwebhook_border_color'] ?? '#ffae00')); ?>
<span class="color-chip" style="--chip-color: <?php echo htmlspecialchars($webhookBorderColor, ENT_QUOTES, 'UTF-8'); ?>;">
<?php echo htmlspecialchars($webhookBorderColor, ENT_QUOTES, 'UTF-8'); ?>
</span>
</td>
<td>
<div class="actions-inline">
<button
type="button"
class="btn-modern btn-mini"
onclick='editWebhook(<?php echo json_encode([
'id' => (int) $webhook['cl_scwebhook_id'],
'name' => (string) $webhook['cl_scwebhook_name'],
'url' => (string) $webhook['cl_scwebhook_url'],
'image_url' => (string) ($webhook['cl_scwebhook_image_url'] ?? ''),
'border_color' => (string) ($webhook['cl_scwebhook_border_color'] ?? '#ffae00'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>)'
>Éditer</button>
<form method="post" onsubmit="return confirm('Supprimer ce canal Discord ?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_webhook">
<input type="hidden" name="webhook_id" value="<?php echo (int) $webhook['cl_scwebhook_id']; ?>">
<button type="submit" class="btn-modern btn-mini danger">Supprimer</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
</main>
</div>
</div>
<script>
function editWebhook(webhook) {
document.getElementById('webhookFormTitle').textContent = 'Modifier le canal Discord';
document.getElementById('webhookAction').value = 'update_webhook';
document.getElementById('webhookId').value = webhook.id || '';
document.getElementById('cl_scwebhook_name').value = webhook.name || '';
document.getElementById('cl_scwebhook_url').value = webhook.url || '';
document.getElementById('cl_scwebhook_image_url').value = webhook.image_url || '';
document.getElementById('cl_scwebhook_border_color').value = webhook.border_color || '#ffae00';
document.getElementById('webhookSubmitBtn').textContent = 'Mettre à jour le canal';
document.getElementById('webhookCancelBtn').style.display = 'inline-flex';
}
function resetWebhookForm() {
document.getElementById('webhookFormTitle').textContent = 'Nouveau canal Discord';
document.getElementById('webhookAction').value = 'add_webhook';
document.getElementById('webhookId').value = '';
document.getElementById('cl_scwebhook_name').value = '';
document.getElementById('cl_scwebhook_url').value = '';
document.getElementById('cl_scwebhook_image_url').value = '';
document.getElementById('cl_scwebhook_border_color').value = '#ffae00';
document.getElementById('webhookSubmitBtn').textContent = 'Ajouter le canal';
document.getElementById('webhookCancelBtn').style.display = 'none';
}
</script>
</body>
</html>