39514-vm/sccharacters.php
Flatlogic Bot 6b68ae0708 V1.3.2
2026-04-16 11:54:19 +00:00

3813 lines
167 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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();
auth_handle_page_access_post('sccharacters.php', 'Personnages');
auth_require_page_access('sccharacters.php', 'Personnages');
scstatsitem_bootstrap();
scitemcustom_bootstrap();
sccharacters_bootstrap();
function sccharacters_current_owner_auth_id(PDO $db): int
{
$session_user = auth_current_user();
if ($session_user === '') {
return 0;
}
$stmt = $db->prepare(
'SELECT cl_auth_id
FROM tbl_auth
WHERE cl_auth_user = :user
LIMIT 1'
);
$stmt->execute(['user' => $session_user]);
return (int) $stmt->fetchColumn();
}
function sccharacters_clean_text(?string $value): string
{
return trim((string) $value);
}
function sccharacters_normalize_item_quantity($value): ?int
{
if ($value === null) {
return null;
}
$value = trim((string) $value);
if ($value === '' || !preg_match('/^\d+$/', $value)) {
return null;
}
$quantity = (int) $value;
if ($quantity <= 0) {
return null;
}
return min($quantity, 999999);
}
function sccharacters_is_valid_url(string $value): bool
{
if ($value === '') {
return true;
}
return filter_var($value, FILTER_VALIDATE_URL) !== false;
}
function sccharacters_excerpt(string $value, int $limit = 120): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if (function_exists('mb_strimwidth')) {
return mb_strimwidth($value, 0, $limit, '…', 'UTF-8');
}
if (strlen($value) <= $limit) {
return $value;
}
return rtrim(substr($value, 0, max(0, $limit - 1))) . '…';
}
function sccharacters_display_stat_value($value): string
{
if (!is_numeric((string) $value)) {
return '0';
}
$formatted = number_format((float) $value, 2, '.', '');
$formatted = rtrim(rtrim($formatted, '0'), '.');
return $formatted === '' ? '0' : $formatted;
}
function sccharacters_custom_stat_preview(array $stat_row): string
{
$sign = (string) ($stat_row['cl_scitemcustomstat_sign'] ?? '');
$prefix = $sign === '-' ? '-' : ($sign === '+' ? '+' : '');
$value = sccharacters_display_stat_value($stat_row['cl_scitemcustomstat_value'] ?? 0);
$unit = trim((string) ($stat_row['cl_scstatsitem_unit'] ?? ''));
return trim((string) ($stat_row['cl_scstatsitem_name'] ?? '') . ' : ' . $prefix . $value . ($unit !== '' ? ' ' . $unit : ''));
}
function sccharacters_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 . '/sccharacter.php?share=' . rawurlencode($token);
}
function sccharacters_find_owned_character(PDO $db, int $character_id, int $owner_auth_id): ?array
{
if ($character_id <= 0 || $owner_auth_id <= 0) {
return null;
}
$stmt = $db->prepare(
'SELECT *
FROM tbl_sccharacters
WHERE cl_sccharacter_id = :id
AND cl_sccharacter_owner_auth_id = :owner_auth_id
LIMIT 1'
);
$stmt->execute([
'id' => $character_id,
'owner_auth_id' => $owner_auth_id,
]);
$row = $stmt->fetch();
return $row ?: null;
}
function sccharacters_allowed_item_per_page(): array
{
return [25, 50, 100, 200];
}
function sccharacters_normalize_item_per_page($value): int
{
$per_page = (int) $value;
$allowed = sccharacters_allowed_item_per_page();
return in_array($per_page, $allowed, true) ? $per_page : 50;
}
function sccharacters_build_return_url(
int $character_id = 0,
string $item_source = '',
string $item_search = '',
bool $keep_item_panel = false,
int $item_page = 1,
int $item_per_page = 50
): string {
$params = [];
if ($character_id > 0) {
$params['character'] = $character_id;
}
if (in_array($item_source, ['base', 'custom'], true)) {
$params['item_source'] = $item_source;
}
if ($item_search !== '') {
$params['item_search'] = $item_search;
}
$item_page = max(1, $item_page);
if ($item_page > 1) {
$params['item_page'] = $item_page;
}
$item_per_page = sccharacters_normalize_item_per_page($item_per_page);
if ($item_per_page !== 50) {
$params['item_per_page'] = $item_per_page;
}
if ($keep_item_panel) {
$params['item_panel'] = '1';
}
return 'sccharacters.php' . ($params !== [] ? '?' . http_build_query($params) : '');
}
function sccharacters_staged_items_session_key(int $owner_auth_id, int $character_id): string
{
return $owner_auth_id . ':' . $character_id;
}
function sccharacters_get_staged_items(int $owner_auth_id, int $character_id): array
{
if ($owner_auth_id <= 0 || $character_id <= 0) {
return [];
}
if (!isset($_SESSION['sccharacters_staged_items']) || !is_array($_SESSION['sccharacters_staged_items'])) {
$_SESSION['sccharacters_staged_items'] = [];
}
$session_key = sccharacters_staged_items_session_key($owner_auth_id, $character_id);
$items = $_SESSION['sccharacters_staged_items'][$session_key] ?? [];
return is_array($items) ? $items : [];
}
function sccharacters_save_staged_items(int $owner_auth_id, int $character_id, array $items): void
{
if (!isset($_SESSION['sccharacters_staged_items']) || !is_array($_SESSION['sccharacters_staged_items'])) {
$_SESSION['sccharacters_staged_items'] = [];
}
$session_key = sccharacters_staged_items_session_key($owner_auth_id, $character_id);
if ($owner_auth_id <= 0 || $character_id <= 0 || $items === []) {
unset($_SESSION['sccharacters_staged_items'][$session_key]);
return;
}
$_SESSION['sccharacters_staged_items'][$session_key] = $items;
}
function sccharacters_parse_item_key(string $item_key): ?array
{
if (!preg_match('/^(base|custom):(\d+)$/', $item_key, $matches)) {
return null;
}
return [
'source' => (string) $matches[1],
'source_id' => (int) $matches[2],
];
}
function sccharacters_merge_selected_items(array $staged_items, array $selected_items, array $item_slots, array $item_notes, array $item_quantities = []): array
{
$selected_items = array_values(array_unique(array_map('strval', $selected_items)));
$staged_count = 0;
$error_count = 0;
foreach ($selected_items as $item_key) {
$parsed = sccharacters_parse_item_key($item_key);
if (!$parsed) {
$error_count++;
continue;
}
$staged_items[$item_key] = [
'source' => $parsed['source'],
'source_id' => $parsed['source_id'],
'category' => sccharacters_clean_text($item_slots[$item_key] ?? ''),
'note' => sccharacters_clean_text($item_notes[$item_key] ?? ''),
'quantity' => sccharacters_normalize_item_quantity($item_quantities[$item_key] ?? null),
];
$staged_count++;
}
return [
'items' => $staged_items,
'staged_count' => $staged_count,
'error_count' => $error_count,
];
}
function sccharacters_fetch_staged_item_details(PDO $db, int $owner_auth_id, array $staged_items): array
{
$details = [];
$base_ids = [];
$custom_ids = [];
foreach ($staged_items as $item_key => $item_data) {
$parsed = sccharacters_parse_item_key((string) $item_key);
if (!$parsed) {
continue;
}
if ($parsed['source'] === 'custom') {
$custom_ids[] = $parsed['source_id'];
} else {
$base_ids[] = $parsed['source_id'];
}
}
$base_ids = array_values(array_unique(array_filter($base_ids)));
$custom_ids = array_values(array_unique(array_filter($custom_ids)));
if ($base_ids !== []) {
$placeholders = implode(',', array_fill(0, count($base_ids), '?'));
$stmt = $db->prepare(
"SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid
FROM tbl_scobjs
WHERE cl_scobjs_id IN ({$placeholders})"
);
$stmt->execute($base_ids);
foreach ($stmt->fetchAll() as $row) {
$item_key = 'base:' . (int) $row['cl_scobjs_id'];
$details[$item_key] = [
'item_key' => $item_key,
'source' => 'base',
'source_id' => (int) $row['cl_scobjs_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'] ?? ''),
'is_custom' => false,
'stat_count' => null,
];
}
}
if ($custom_ids !== []) {
$placeholders = implode(',', array_fill(0, count($custom_ids), '?'));
$stmt = $db->prepare(
"SELECT c.cl_scitemcustom_id,
o.cl_scobjs_name,
o.cl_scobjs_type,
o.cl_scobjs_subtype,
o.cl_scobjs_uuid,
COUNT(cs.cl_scitemcustomstat_id) AS cl_scitemcustom_stat_count
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
LEFT JOIN tbl_scitemcustomstat cs ON cs.cl_scitemcustomstat_itemcustom_id = c.cl_scitemcustom_id
WHERE c.cl_scitemcustom_owner_auth_id = ?
AND c.cl_scitemcustom_id IN ({$placeholders})
GROUP BY c.cl_scitemcustom_id"
);
$stmt->execute(array_merge([$owner_auth_id], $custom_ids));
foreach ($stmt->fetchAll() as $row) {
$item_key = 'custom:' . (int) $row['cl_scitemcustom_id'];
$details[$item_key] = [
'item_key' => $item_key,
'source' => 'custom',
'source_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'] ?? ''),
'is_custom' => true,
'stat_count' => (int) ($row['cl_scitemcustom_stat_count'] ?? 0),
];
}
}
return $details;
}
function sccharacters_fetch_owned_character_item_ids(PDO $db, int $owner_auth_id, int $character_id): array
{
if ($character_id <= 0 || $owner_auth_id <= 0) {
return [];
}
sccharacters_reindex_character_items($db, $character_id);
$stmt = $db->prepare(
'SELECT ci.cl_sccharacteritem_id
FROM tbl_sccharacteritems ci
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
WHERE ci.cl_sccharacteritem_character_id = :character_id
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC'
);
$stmt->execute([
'character_id' => $character_id,
'owner_auth_id' => $owner_auth_id,
]);
return array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
}
function sccharacters_save_character_item_order(PDO $db, int $character_id, array $ordered_item_ids): void
{
if ($character_id <= 0 || $ordered_item_ids === []) {
return;
}
$stmt = $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 ($ordered_item_ids as $item_id) {
$stmt->execute([
'sort_order' => $position,
'character_id' => $character_id,
'item_id' => (int) $item_id,
]);
$position++;
}
$db->commit();
} catch (Throwable $exception) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $exception;
}
}
function sccharacters_move_owned_character_item(
PDO $db,
int $owner_auth_id,
int $character_id,
int $character_item_id,
string $direction
): bool {
$ordered_item_ids = sccharacters_fetch_owned_character_item_ids($db, $owner_auth_id, $character_id);
if ($ordered_item_ids === []) {
return false;
}
$current_index = array_search($character_item_id, $ordered_item_ids, true);
if ($current_index === false) {
return false;
}
$target_index = $current_index;
$last_index = count($ordered_item_ids) - 1;
switch ($direction) {
case 'top':
$target_index = 0;
break;
case 'up':
$target_index = max(0, $current_index - 1);
break;
case 'down':
$target_index = min($last_index, $current_index + 1);
break;
case 'bottom':
$target_index = $last_index;
break;
default:
return false;
}
if ($target_index === $current_index) {
return true;
}
$moving_item_id = $ordered_item_ids[$current_index];
array_splice($ordered_item_ids, $current_index, 1);
array_splice($ordered_item_ids, $target_index, 0, [$moving_item_id]);
sccharacters_save_character_item_order($db, $character_id, $ordered_item_ids);
return true;
}
function sccharacters_fetch_owned_character_item_rows(PDO $db, int $owner_auth_id, int $character_id): array
{
if ($character_id <= 0 || $owner_auth_id <= 0) {
return [];
}
sccharacters_reindex_character_items($db, $character_id);
$stmt = $db->prepare(
"SELECT
ci.cl_sccharacteritem_id,
ci.cl_sccharacteritem_slot,
ci.cl_sccharacteritem_source,
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype
FROM tbl_sccharacteritems ci
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
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
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id
ORDER BY ci.cl_sccharacteritem_sort_order ASC, ci.cl_sccharacteritem_id ASC"
);
$stmt->execute([
'character_id' => $character_id,
'owner_auth_id' => $owner_auth_id,
]);
return $stmt->fetchAll();
}
function sccharacters_group_item_ids_by_category(array $item_rows): array
{
$grouped = [];
foreach ($item_rows as $item_row) {
$is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$item_type = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
$item_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'] ?? ''),
$item_type,
$item_subtype
);
if (!isset($grouped[$category_key])) {
$grouped[$category_key] = [];
}
$grouped[$category_key][] = (int) ($item_row['cl_sccharacteritem_id'] ?? 0);
}
return $grouped;
}
function sccharacters_flatten_grouped_item_ids(array $grouped_item_ids, array $category_order): array
{
$ordered_ids = [];
$sorted_groups = sccharacters_sort_items_by_category_order($grouped_item_ids, $category_order);
foreach ($sorted_groups as $category_item_ids) {
foreach ($category_item_ids as $item_id) {
$item_id = (int) $item_id;
if ($item_id > 0) {
$ordered_ids[] = $item_id;
}
}
}
return $ordered_ids;
}
function sccharacters_move_owned_character_category(
PDO $db,
int $owner_auth_id,
int $character_id,
string $category_key,
string $direction
): bool {
$character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
if (!$character) {
return false;
}
$grouped_item_ids = sccharacters_group_item_ids_by_category(
sccharacters_fetch_owned_character_item_rows($db, $owner_auth_id, $character_id)
);
if (!isset($grouped_item_ids[$category_key])) {
return false;
}
$category_order = sccharacters_character_category_order($character);
$visible_order = [];
foreach ($category_order as $ordered_category_key) {
if (isset($grouped_item_ids[$ordered_category_key])) {
$visible_order[] = $ordered_category_key;
}
}
if ($visible_order === []) {
return false;
}
$current_index = array_search($category_key, $visible_order, true);
if ($current_index === false) {
return false;
}
$target_index = $current_index;
if ($direction === 'up') {
$target_index = max(0, $current_index - 1);
} elseif ($direction === 'down') {
$target_index = min(count($visible_order) - 1, $current_index + 1);
} else {
return false;
}
if ($target_index === $current_index) {
return true;
}
$moving_category_key = $visible_order[$current_index];
array_splice($visible_order, $current_index, 1);
array_splice($visible_order, $target_index, 0, [$moving_category_key]);
$visible_lookup = array_fill_keys(array_keys($grouped_item_ids), true);
$visible_pointer = 0;
$rebuilt_order = [];
foreach ($category_order as $ordered_category_key) {
if (isset($visible_lookup[$ordered_category_key])) {
$rebuilt_order[] = $visible_order[$visible_pointer] ?? $ordered_category_key;
$visible_pointer++;
} else {
$rebuilt_order[] = $ordered_category_key;
}
}
sccharacters_save_character_category_order($db, $character_id, $rebuilt_order);
sccharacters_save_character_item_order(
$db,
$character_id,
sccharacters_flatten_grouped_item_ids($grouped_item_ids, $rebuilt_order)
);
return true;
}
function sccharacters_reorder_owned_character_items(
PDO $db,
int $owner_auth_id,
int $character_id,
string $category_key,
array $ordered_item_ids
): bool {
$character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
if (!$character) {
return false;
}
$grouped_item_ids = sccharacters_group_item_ids_by_category(
sccharacters_fetch_owned_character_item_rows($db, $owner_auth_id, $character_id)
);
if (!isset($grouped_item_ids[$category_key])) {
return false;
}
$current_item_ids = array_map('intval', $grouped_item_ids[$category_key]);
$posted_item_ids = array_values(array_filter(array_map('intval', $ordered_item_ids), static fn ($item_id) => $item_id > 0));
sort($current_item_ids);
$sorted_posted = $posted_item_ids;
sort($sorted_posted);
if ($posted_item_ids === [] || $sorted_posted !== $current_item_ids) {
return false;
}
$grouped_item_ids[$category_key] = $posted_item_ids;
sccharacters_save_character_item_order(
$db,
$character_id,
sccharacters_flatten_grouped_item_ids($grouped_item_ids, sccharacters_character_category_order($character))
);
return true;
}
function sccharacters_attach_item(
PDO $db,
int $owner_auth_id,
int $character_id,
string $source,
int $source_id,
string $requested_category,
?int $quantity,
string $note,
?string &$error_message = null
): bool {
if ($character_id <= 0 || $owner_auth_id <= 0 || $source_id <= 0) {
$error_message = 'Paramètres dajout invalides.';
return false;
}
$character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
if (!$character) {
$error_message = 'Personnage introuvable.';
return false;
}
$sort_order = sccharacters_next_item_sort_order($db, $character_id);
if ($source === 'base') {
$stmt_source = $db->prepare(
'SELECT cl_scobjs_id AS item_id, cl_scobjs_type, cl_scobjs_subtype
FROM tbl_scobjs
WHERE cl_scobjs_id = :id
LIMIT 1'
);
$stmt_source->execute(['id' => $source_id]);
$item_row = $stmt_source->fetch();
if (!$item_row) {
$error_message = 'Objet de base introuvable.';
return false;
}
$category = sccharacters_resolve_item_category(
$requested_category,
(string) ($item_row['cl_scobjs_type'] ?? ''),
(string) ($item_row['cl_scobjs_subtype'] ?? '')
);
$stmt_insert = $db->prepare(
'INSERT INTO tbl_sccharacteritems (
cl_sccharacteritem_character_id,
cl_sccharacteritem_source,
cl_sccharacteritem_scobjs_id,
cl_sccharacteritem_scitemcustom_id,
cl_sccharacteritem_slot,
cl_sccharacteritem_quantity,
cl_sccharacteritem_note,
cl_sccharacteritem_sort_order
) VALUES (
:character_id,
:source,
:scobjs_id,
NULL,
:slot,
:quantity,
:note,
:sort_order
)'
);
$stmt_insert->execute([
'character_id' => $character_id,
'source' => 'base',
'scobjs_id' => $source_id,
'slot' => $category,
'quantity' => $quantity,
'note' => $note !== '' ? $note : null,
'sort_order' => $sort_order,
]);
return true;
}
if ($source === 'custom') {
$stmt_source = $db->prepare(
'SELECT c.cl_scitemcustom_id AS item_id, o.cl_scobjs_type, o.cl_scobjs_subtype
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
WHERE c.cl_scitemcustom_id = :id
AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id
LIMIT 1'
);
$stmt_source->execute([
'id' => $source_id,
'owner_auth_id' => $owner_auth_id,
]);
$item_row = $stmt_source->fetch();
if (!$item_row) {
$error_message = 'Objet personnalisé introuvable ou non autorisé.';
return false;
}
$category = sccharacters_resolve_item_category(
$requested_category,
(string) ($item_row['cl_scobjs_type'] ?? ''),
(string) ($item_row['cl_scobjs_subtype'] ?? '')
);
$stmt_insert = $db->prepare(
'INSERT INTO tbl_sccharacteritems (
cl_sccharacteritem_character_id,
cl_sccharacteritem_source,
cl_sccharacteritem_scobjs_id,
cl_sccharacteritem_scitemcustom_id,
cl_sccharacteritem_slot,
cl_sccharacteritem_quantity,
cl_sccharacteritem_note,
cl_sccharacteritem_sort_order
) VALUES (
:character_id,
:source,
NULL,
:scitemcustom_id,
:slot,
:quantity,
:note,
:sort_order
)'
);
$stmt_insert->execute([
'character_id' => $character_id,
'source' => 'custom',
'scitemcustom_id' => $source_id,
'slot' => $category,
'quantity' => $quantity,
'note' => $note !== '' ? $note : null,
'sort_order' => $sort_order,
]);
return true;
}
$error_message = 'Source dobjet invalide.';
return false;
}
function sccharacters_update_character_item(
PDO $db,
int $owner_auth_id,
int $character_id,
int $character_item_id,
string $requested_category,
?int $quantity,
string $note,
?string &$error_message = null
): bool {
if ($character_id <= 0 || $owner_auth_id <= 0 || $character_item_id <= 0) {
$error_message = 'Paramètres de mise à jour invalides.';
return false;
}
$stmt_item = $db->prepare(
"SELECT
ci.cl_sccharacteritem_id,
ci.cl_sccharacteritem_source,
bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype
FROM tbl_sccharacteritems ci
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
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_id = :character_item_id
AND c.cl_sccharacter_id = :character_id
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id
LIMIT 1"
);
$stmt_item->execute([
'character_item_id' => $character_item_id,
'character_id' => $character_id,
'owner_auth_id' => $owner_auth_id,
]);
$item_row = $stmt_item->fetch();
if (!$item_row) {
$error_message = 'Objet introuvable ou non autorisé.';
return false;
}
$is_custom = (string) ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$item_type = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
$item_subtype = $is_custom
? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
$category = sccharacters_resolve_item_category($requested_category, $item_type, $item_subtype);
$stmt_update = $db->prepare(
'UPDATE tbl_sccharacteritems
SET cl_sccharacteritem_slot = :slot,
cl_sccharacteritem_quantity = :quantity,
cl_sccharacteritem_note = :note
WHERE cl_sccharacteritem_id = :character_item_id
AND cl_sccharacteritem_character_id = :character_id'
);
$stmt_update->execute([
'slot' => $category,
'quantity' => $quantity,
'note' => $note !== '' ? $note : null,
'character_item_id' => $character_item_id,
'character_id' => $character_id,
]);
return true;
}
$flash = auth_flash_get();
$flash_type = $flash['type'] ?? '';
$flash_message = $flash['message'] ?? '';
$db = db();
$csrf_token = auth_csrf_token();
$current_owner_auth_id = sccharacters_current_owner_auth_id($db);
$current_session_user = auth_current_user();
$current_session_role = auth_current_role();
$role_label = auth_role_label($current_session_role);
if ($current_owner_auth_id <= 0) {
auth_flash_set('error', 'Impossible didentifier le compte connecté.');
header('Location: index.php');
exit;
}
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: sccharacters.php');
exit;
}
$action = (string) ($_POST['action'] ?? '');
if ($action === 'create_character') {
$name = sccharacters_clean_text($_POST['character_name'] ?? '');
$role = sccharacters_clean_text($_POST['character_role'] ?? '');
$faction = '';
$org_rsi_url = sccharacters_clean_text($_POST['character_org_rsi_url'] ?? '');
$player_handle = sccharacters_clean_text($_POST['character_player_handle'] ?? '');
$avatar_url = sccharacters_clean_text($_POST['character_avatar_url'] ?? '');
$description = sccharacters_clean_text($_POST['character_description'] ?? '');
$notes = sccharacters_clean_text($_POST['character_notes'] ?? '');
$share_enabled = isset($_POST['character_share_enabled']) ? 1 : 0;
$is_pinned = isset($_POST['character_is_pinned']) ? 1 : 0;
$posted_category_order_state = sccharacters_clean_text($_POST['category_order_state'] ?? '');
$posted_item_order_state_raw = trim((string) ($_POST['item_order_state'] ?? ''));
if ($name === '') {
auth_flash_set('error', 'Le nom du personnage est obligatoire.');
header('Location: sccharacters.php?mode=create');
exit;
}
if (!sccharacters_is_valid_url($avatar_url)) {
auth_flash_set('error', 'LURL de lavatar nest pas valide.');
header('Location: sccharacters.php?mode=create');
exit;
}
if (!sccharacters_is_valid_url($org_rsi_url)) {
auth_flash_set('error', 'LURL RSI de lorganisation nest pas valide.');
header('Location: sccharacters.php?mode=create');
exit;
}
$stmt = $db->prepare(
'INSERT INTO tbl_sccharacters (
cl_sccharacter_owner_auth_id,
cl_sccharacter_name,
cl_sccharacter_role,
cl_sccharacter_faction,
cl_sccharacter_org_rsi_url,
cl_sccharacter_player_handle,
cl_sccharacter_avatar_url,
cl_sccharacter_description,
cl_sccharacter_notes,
cl_sccharacter_share_token,
cl_sccharacter_share_enabled,
cl_sccharacter_is_pinned
) VALUES (
:owner_auth_id,
:name,
:role,
:faction,
:org_rsi_url,
:player_handle,
:avatar_url,
:description,
:notes,
:share_token,
:share_enabled,
:is_pinned
)'
);
$stmt->execute([
'owner_auth_id' => $current_owner_auth_id,
'name' => $name,
'role' => $role,
'faction' => $faction,
'org_rsi_url' => $org_rsi_url,
'player_handle' => $player_handle,
'avatar_url' => $avatar_url,
'description' => $description !== '' ? $description : null,
'notes' => $notes !== '' ? $notes : null,
'share_token' => sccharacters_generate_share_token($db),
'share_enabled' => $share_enabled,
'is_pinned' => $is_pinned,
]);
$new_character_id = (int) $db->lastInsertId();
auth_flash_set('success', 'Personnage créé avec succès.');
header('Location: sccharacters.php?character=' . $new_character_id);
exit;
}
if ($action === 'update_character') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
if (!$character) {
auth_flash_set('error', 'Personnage introuvable.');
header('Location: sccharacters.php');
exit;
}
$name = sccharacters_clean_text($_POST['character_name'] ?? '');
$role = sccharacters_clean_text($_POST['character_role'] ?? '');
$faction = '';
$org_rsi_url = sccharacters_clean_text($_POST['character_org_rsi_url'] ?? '');
$player_handle = sccharacters_clean_text($_POST['character_player_handle'] ?? '');
$avatar_url = sccharacters_clean_text($_POST['character_avatar_url'] ?? '');
$description = sccharacters_clean_text($_POST['character_description'] ?? '');
$notes = sccharacters_clean_text($_POST['character_notes'] ?? '');
$share_enabled = isset($_POST['character_share_enabled']) ? 1 : 0;
$is_pinned = isset($_POST['character_is_pinned']) ? 1 : 0;
$posted_category_order_state = sccharacters_clean_text($_POST['category_order_state'] ?? '');
$posted_item_order_state_raw = trim((string) ($_POST['item_order_state'] ?? ''));
if ($name === '') {
auth_flash_set('error', 'Le nom du personnage est obligatoire.');
header('Location: sccharacters.php?character=' . $character_id);
exit;
}
if (!sccharacters_is_valid_url($avatar_url)) {
auth_flash_set('error', 'LURL de lavatar nest pas valide.');
header('Location: sccharacters.php?character=' . $character_id);
exit;
}
if (!sccharacters_is_valid_url($org_rsi_url)) {
auth_flash_set('error', 'LURL RSI de lorganisation nest pas valide.');
header('Location: sccharacters.php?character=' . $character_id);
exit;
}
$stmt = $db->prepare(
'UPDATE tbl_sccharacters
SET cl_sccharacter_name = :name,
cl_sccharacter_role = :role,
cl_sccharacter_faction = :faction,
cl_sccharacter_org_rsi_url = :org_rsi_url,
cl_sccharacter_player_handle = :player_handle,
cl_sccharacter_avatar_url = :avatar_url,
cl_sccharacter_description = :description,
cl_sccharacter_notes = :notes,
cl_sccharacter_share_enabled = :share_enabled,
cl_sccharacter_is_pinned = :is_pinned
WHERE cl_sccharacter_id = :id
AND cl_sccharacter_owner_auth_id = :owner_auth_id'
);
$stmt->execute([
'name' => $name,
'role' => $role,
'faction' => $faction,
'org_rsi_url' => $org_rsi_url,
'player_handle' => $player_handle,
'avatar_url' => $avatar_url,
'description' => $description !== '' ? $description : null,
'notes' => $notes !== '' ? $notes : null,
'share_enabled' => $share_enabled,
'is_pinned' => $is_pinned,
'id' => $character_id,
'owner_auth_id' => $current_owner_auth_id,
]);
$current_item_rows = sccharacters_fetch_owned_character_item_rows($db, $current_owner_auth_id, $character_id);
$grouped_item_ids = sccharacters_group_item_ids_by_category($current_item_rows);
$current_visible_category_keys = array_keys(
sccharacters_sort_items_by_category_order($grouped_item_ids, sccharacters_character_category_order($character))
);
$submitted_visible_category_order = $current_visible_category_keys;
if ($posted_category_order_state !== '') {
$candidate_category_order = array_values(array_filter(
array_map('sccharacters_clean_text', preg_split('/\s*,\s*/', $posted_category_order_state, -1, PREG_SPLIT_NO_EMPTY) ?: []),
static fn (string $category_key): bool => $category_key !== ''
));
if ($candidate_category_order !== []) {
$candidate_sorted = $candidate_category_order;
$current_sorted = $current_visible_category_keys;
sort($candidate_sorted);
sort($current_sorted);
if ($candidate_sorted === $current_sorted) {
$submitted_visible_category_order = $candidate_category_order;
}
}
}
$submitted_item_order_state = [];
if ($posted_item_order_state_raw !== '') {
$decoded_item_order_state = json_decode($posted_item_order_state_raw, true);
if (is_array($decoded_item_order_state)) {
$submitted_item_order_state = $decoded_item_order_state;
}
}
foreach ($grouped_item_ids as $category_key => $current_category_item_ids) {
$current_category_item_ids = array_values(array_map('intval', $current_category_item_ids));
$candidate_category_item_ids = $submitted_item_order_state[$category_key] ?? null;
if (!is_array($candidate_category_item_ids)) {
$grouped_item_ids[$category_key] = $current_category_item_ids;
continue;
}
$candidate_category_item_ids = array_values(array_filter(
array_map('intval', $candidate_category_item_ids),
static fn (int $item_id): bool => $item_id > 0
));
$sorted_candidate_item_ids = $candidate_category_item_ids;
$sorted_current_category_item_ids = $current_category_item_ids;
sort($sorted_candidate_item_ids);
sort($sorted_current_category_item_ids);
if ($candidate_category_item_ids !== [] && $sorted_candidate_item_ids === $sorted_current_category_item_ids) {
$grouped_item_ids[$category_key] = $candidate_category_item_ids;
} else {
$grouped_item_ids[$category_key] = $current_category_item_ids;
}
}
$persisted_category_order = sccharacters_character_category_order($character);
$visible_category_lookup = array_fill_keys(array_keys($grouped_item_ids), true);
$visible_pointer = 0;
$rebuilt_category_order = [];
foreach ($persisted_category_order as $ordered_category_key) {
if (isset($visible_category_lookup[$ordered_category_key])) {
$rebuilt_category_order[] = $submitted_visible_category_order[$visible_pointer] ?? $ordered_category_key;
$visible_pointer++;
} else {
$rebuilt_category_order[] = $ordered_category_key;
}
}
sccharacters_save_character_category_order($db, $character_id, $rebuilt_category_order);
sccharacters_save_character_item_order(
$db,
$character_id,
sccharacters_flatten_grouped_item_ids($grouped_item_ids, $rebuilt_category_order)
);
auth_flash_set('success', 'Personnage mis à jour.');
header('Location: sccharacters.php?character=' . $character_id);
exit;
}
if ($action === 'delete_character') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
if (!$character) {
auth_flash_set('error', 'Personnage introuvable.');
header('Location: sccharacters.php');
exit;
}
$stmt = $db->prepare(
'DELETE FROM tbl_sccharacters
WHERE cl_sccharacter_id = :id
AND cl_sccharacter_owner_auth_id = :owner_auth_id'
);
$stmt->execute([
'id' => $character_id,
'owner_auth_id' => $current_owner_auth_id,
]);
auth_flash_set('success', 'Personnage supprimé.');
header('Location: sccharacters.php');
exit;
}
if ($action === 'regenerate_share_token') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
if (!$character) {
auth_flash_set('error', 'Personnage introuvable.');
header('Location: sccharacters.php');
exit;
}
$stmt = $db->prepare(
'UPDATE tbl_sccharacters
SET cl_sccharacter_share_token = :token
WHERE cl_sccharacter_id = :id
AND cl_sccharacter_owner_auth_id = :owner_auth_id'
);
$stmt->execute([
'token' => sccharacters_generate_share_token($db),
'id' => $character_id,
'owner_auth_id' => $current_owner_auth_id,
]);
auth_flash_set('success', 'Lien public régénéré.');
header('Location: sccharacters.php?character=' . $character_id);
exit;
}
if ($action === 'add_base_item') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$obj_id = (int) ($_POST['base_obj_id'] ?? 0);
$requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
$quantity = sccharacters_normalize_item_quantity($_POST['item_quantity'] ?? null);
$note = sccharacters_clean_text($_POST['item_note'] ?? '');
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
$error_message = null;
if (!sccharacters_attach_item(
$db,
$current_owner_auth_id,
$character_id,
'base',
$obj_id,
$requested_category,
$quantity,
$note,
$error_message
)) {
auth_flash_set('error', $error_message ?? 'Impossible dajouter lobjet.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
exit;
}
auth_flash_set('success', 'Objet de la base ajouté au personnage.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
exit;
}
if ($action === 'add_custom_item') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$itemcustom_id = (int) ($_POST['custom_item_id'] ?? 0);
$requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
$quantity = sccharacters_normalize_item_quantity($_POST['item_quantity'] ?? null);
$note = sccharacters_clean_text($_POST['item_note'] ?? '');
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'custom');
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
$error_message = null;
if (!sccharacters_attach_item(
$db,
$current_owner_auth_id,
$character_id,
'custom',
$itemcustom_id,
$requested_category,
$quantity,
$note,
$error_message
)) {
auth_flash_set('error', $error_message ?? 'Impossible dajouter lobjet.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
exit;
}
auth_flash_set('success', 'Objet personnalisé ajouté au personnage.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true, $item_page_context, $item_per_page_context));
exit;
}
if ($action === 'update_character_item') {
$character_item_id = (int) ($_POST['character_item_id'] ?? 0);
$character_id = (int) ($_POST['character_id'] ?? 0);
$requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
$quantity = sccharacters_normalize_item_quantity($_POST['item_quantity'] ?? null);
$note = sccharacters_clean_text($_POST['item_note'] ?? '');
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
$error_message = null;
if (!sccharacters_update_character_item(
$db,
$current_owner_auth_id,
$character_id,
$character_item_id,
$requested_category,
$quantity,
$note,
$error_message
)) {
auth_flash_set('error', $error_message ?? 'Impossible de mettre à jour cet objet.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
auth_flash_set('success', 'Objet mis à jour.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
if ($action === 'move_character_category') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$category_key = sccharacters_clean_text($_POST['category_key'] ?? '');
$move_direction = sccharacters_clean_text($_POST['move_direction'] ?? '');
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
if (!sccharacters_move_owned_character_category($db, $current_owner_auth_id, $character_id, $category_key, $move_direction)) {
auth_flash_set('error', 'Impossible de déplacer cette catégorie.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
auth_flash_set('success', 'Ordre des catégories mis à jour.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
if ($action === 'reorder_character_items') {
$character_id = (int) ($_POST['character_id'] ?? 0);
$category_key = sccharacters_clean_text($_POST['category_key'] ?? '');
$ordered_item_ids = preg_split('/\s*,\s*/', (string) ($_POST['ordered_item_ids'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
if (!sccharacters_reorder_owned_character_items($db, $current_owner_auth_id, $character_id, $category_key, $ordered_item_ids)) {
auth_flash_set('error', 'Impossible de réordonner les objets.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
auth_flash_set('success', 'Ordre des objets mis à jour.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
if ($action === 'delete_character_item') {
$character_item_id = (int) ($_POST['character_item_id'] ?? 0);
$character_id = (int) ($_POST['character_id'] ?? 0);
$item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
$item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
$item_page_context = max(1, (int) ($_POST['item_page_context'] ?? 1));
$item_per_page_context = sccharacters_normalize_item_per_page($_POST['item_per_page_context'] ?? 50);
$item_panel_context = (string) ($_POST['item_panel_context'] ?? '') === '1';
$stmt = $db->prepare(
'DELETE ci
FROM tbl_sccharacteritems ci
INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
WHERE ci.cl_sccharacteritem_id = :character_item_id
AND c.cl_sccharacter_id = :character_id
AND c.cl_sccharacter_owner_auth_id = :owner_auth_id'
);
$stmt->execute([
'character_item_id' => $character_item_id,
'character_id' => $character_id,
'owner_auth_id' => $current_owner_auth_id,
]);
sccharacters_reindex_character_items($db, $character_id);
auth_flash_set('success', 'Objet retiré du personnage.');
header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, $item_panel_context, $item_page_context, $item_per_page_context));
exit;
}
}
$stmt_characters = $db->prepare(
'SELECT c.*, COUNT(ci.cl_sccharacteritem_id) AS cl_sccharacter_item_count
FROM tbl_sccharacters c
LEFT JOIN tbl_sccharacteritems ci ON ci.cl_sccharacteritem_character_id = c.cl_sccharacter_id
WHERE c.cl_sccharacter_owner_auth_id = :owner_auth_id
GROUP BY c.cl_sccharacter_id
ORDER BY c.cl_sccharacter_is_pinned DESC, c.cl_sccharacter_updated_at DESC, c.cl_sccharacter_name ASC'
);
$stmt_characters->execute(['owner_auth_id' => $current_owner_auth_id]);
$characters = $stmt_characters->fetchAll();
$character_lookup = [];
foreach ($characters as $character_row) {
$character_lookup[(int) $character_row['cl_sccharacter_id']] = $character_row;
}
$mode = (string) ($_GET['mode'] ?? '');
$selected_character_id = (int) ($_GET['character'] ?? 0);
$selected_character = null;
if ($selected_character_id > 0 && isset($character_lookup[$selected_character_id])) {
$selected_character = $character_lookup[$selected_character_id];
} elseif ($characters !== []) {
$selected_character = $characters[0];
$selected_character_id = (int) $selected_character['cl_sccharacter_id'];
}
$create_panel_open = ($mode === 'create') || ($characters === []);
$item_source = sccharacters_clean_text($_GET['item_source'] ?? 'base');
if (!in_array($item_source, ['base', 'custom'], true)) {
$item_source = 'base';
}
$item_search = sccharacters_clean_text($_GET['item_search'] ?? '');
$item_panel_open = (string) ($_GET['item_panel'] ?? '') === '1';
$item_page = max(1, (int) ($_GET['item_page'] ?? 1));
$item_per_page = sccharacters_normalize_item_per_page($_GET['item_per_page'] ?? 50);
$item_results = [];
$item_result_custom_stats = [];
$item_total_results = 0;
$item_total_pages = 1;
$item_result_offset_start = 0;
$item_result_offset_end = 0;
if ($selected_character) {
if ($item_source === 'custom') {
$count_sql = "SELECT COUNT(DISTINCT c.cl_scitemcustom_id)
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";
$params = ['owner_auth_id' => $current_owner_auth_id];
if ($item_search !== '') {
$count_sql .= " AND (
o.cl_scobjs_name LIKE :search
OR o.cl_scobjs_type LIKE :search
OR o.cl_scobjs_subtype LIKE :search
OR o.cl_scobjs_uuid LIKE :search
)";
$params['search'] = '%' . $item_search . '%';
}
$stmt_item_count = $db->prepare($count_sql);
$stmt_item_count->execute($params);
$item_total_results = (int) $stmt_item_count->fetchColumn();
$item_total_pages = max(1, (int) ceil($item_total_results / $item_per_page));
$item_page = min($item_page, $item_total_pages);
$item_offset = max(0, ($item_page - 1) * $item_per_page);
$sql = "SELECT c.cl_scitemcustom_id,
o.cl_scobjs_name,
o.cl_scobjs_type,
o.cl_scobjs_subtype,
o.cl_scobjs_uuid,
COUNT(cs.cl_scitemcustomstat_id) AS cl_scitemcustom_stat_count
FROM tbl_scitemcustom c
INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
LEFT JOIN tbl_scitemcustomstat cs ON cs.cl_scitemcustomstat_itemcustom_id = c.cl_scitemcustom_id
WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id";
if ($item_search !== '') {
$sql .= " AND (
o.cl_scobjs_name LIKE :search
OR o.cl_scobjs_type LIKE :search
OR o.cl_scobjs_subtype LIKE :search
OR o.cl_scobjs_uuid LIKE :search
)";
}
$sql .= "
GROUP BY c.cl_scitemcustom_id
ORDER BY o.cl_scobjs_name ASC, c.cl_scitemcustom_id ASC
LIMIT " . (int) $item_per_page . " OFFSET " . (int) $item_offset;
$stmt_item_results = $db->prepare($sql);
$stmt_item_results->execute($params);
$item_results = $stmt_item_results->fetchAll();
$item_result_custom_ids = [];
foreach ($item_results as $item_result_row) {
$itemcustom_id = (int) ($item_result_row['cl_scitemcustom_id'] ?? 0);
if ($itemcustom_id > 0) {
$item_result_custom_ids[] = $itemcustom_id;
}
}
$item_result_custom_ids = array_values(array_unique($item_result_custom_ids));
if ($item_result_custom_ids !== []) {
$placeholders = implode(',', array_fill(0, count($item_result_custom_ids), '?'));
$stmt_item_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_item_stats->execute($item_result_custom_ids);
foreach ($stmt_item_stats->fetchAll() as $item_stat_row) {
$itemcustom_id = (int) ($item_stat_row['cl_scitemcustomstat_itemcustom_id'] ?? 0);
if (!isset($item_result_custom_stats[$itemcustom_id])) {
$item_result_custom_stats[$itemcustom_id] = [];
}
$item_result_custom_stats[$itemcustom_id][] = $item_stat_row;
}
}
} else {
$count_sql = "SELECT COUNT(*)
FROM tbl_scobjs
WHERE 1 = 1";
$params = [];
if ($item_search !== '') {
$count_sql .= " AND (
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'] = '%' . $item_search . '%';
}
$stmt_item_count = $db->prepare($count_sql);
$stmt_item_count->execute($params);
$item_total_results = (int) $stmt_item_count->fetchColumn();
$item_total_pages = max(1, (int) ceil($item_total_results / $item_per_page));
$item_page = min($item_page, $item_total_pages);
$item_offset = max(0, ($item_page - 1) * $item_per_page);
$sql = "SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid
FROM tbl_scobjs
WHERE 1 = 1";
if ($item_search !== '') {
$sql .= " AND (
cl_scobjs_name LIKE :search
OR cl_scobjs_type LIKE :search
OR cl_scobjs_subtype LIKE :search
OR cl_scobjs_uuid LIKE :search
)";
}
$sql .= "
ORDER BY cl_scobjs_name ASC
LIMIT " . (int) $item_per_page . " OFFSET " . (int) $item_offset;
$stmt_item_results = $db->prepare($sql);
$stmt_item_results->execute($params);
$item_results = $stmt_item_results->fetchAll();
}
if ($item_total_results > 0) {
$item_result_offset_start = (($item_page - 1) * $item_per_page) + 1;
$item_result_offset_end = min($item_total_results, $item_result_offset_start + count($item_results) - 1);
}
}
$selected_character_items = [];
$custom_stats_by_itemcustom = [];
if ($selected_character) {
sccharacters_reindex_character_items($db, (int) $selected_character['cl_sccharacter_id']);
$stmt_character_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,
co.cl_scitemcustom_id AS cl_sccharacteritem_custom_ref_id,
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_character_items->execute(['character_id' => (int) $selected_character['cl_sccharacter_id']]);
$selected_character_items = $stmt_character_items->fetchAll();
$custom_item_ids = [];
foreach ($selected_character_items as $character_item_row) {
if (($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom' && !empty($character_item_row['cl_sccharacteritem_scitemcustom_id'])) {
$custom_item_ids[] = (int) $character_item_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_custom_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_custom_stats->execute($custom_item_ids);
foreach ($stmt_custom_stats->fetchAll() as $custom_stat_row) {
$itemcustom_id = (int) $custom_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][] = $custom_stat_row;
}
}
}
$item_category_options = sccharacters_item_category_options();
$selected_character_category_order = $selected_character
? sccharacters_character_category_order($selected_character)
: sccharacters_default_category_order();
$selected_character_items_by_category = [];
foreach ($selected_character_items as $character_item_row) {
$is_custom = ($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$item_type = $is_custom
? (string) ($character_item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($character_item_row['cl_sccharacteritem_base_type'] ?? '');
$item_subtype = $is_custom
? (string) ($character_item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($character_item_row['cl_sccharacteritem_base_subtype'] ?? '');
$category_key = sccharacters_resolve_item_category(
(string) ($character_item_row['cl_sccharacteritem_slot'] ?? ''),
$item_type,
$item_subtype
);
if (!isset($selected_character_items_by_category[$category_key])) {
$selected_character_items_by_category[$category_key] = [];
}
$selected_character_items_by_category[$category_key][] = $character_item_row;
}
$selected_character_items_by_category = sccharacters_sort_items_by_category_order(
$selected_character_items_by_category,
$selected_character_category_order
);
$selected_character_category_order_meta = [];
$visible_category_keys = array_keys($selected_character_items_by_category);
$visible_category_count = count($visible_category_keys);
foreach ($visible_category_keys as $visible_category_index => $visible_category_key) {
$selected_character_category_order_meta[$visible_category_key] = [
'is_first' => $visible_category_index === 0,
'is_last' => $visible_category_index === ($visible_category_count - 1),
];
}
$selected_character_visible_category_order_state = implode(',', $visible_category_keys);
$selected_character_item_order_state = [];
foreach ($selected_character_items_by_category as $category_key => $category_items) {
$selected_character_item_order_state[$category_key] = array_values(array_map(
static fn (array $item_row): int => (int) ($item_row['cl_sccharacteritem_id'] ?? 0),
$category_items
));
}
$selected_character_item_order_state_json = json_encode(
$selected_character_item_order_state,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if (!is_string($selected_character_item_order_state_json)) {
$selected_character_item_order_state_json = '{}';
}
$create_character = [
'cl_sccharacter_name' => '',
'cl_sccharacter_role' => '',
'cl_sccharacter_faction' => '',
'cl_sccharacter_org_rsi_url' => '',
'cl_sccharacter_player_handle' => '',
'cl_sccharacter_avatar_url' => '',
'cl_sccharacter_description' => '',
'cl_sccharacter_notes' => '',
'cl_sccharacter_share_enabled' => 0,
'cl_sccharacter_is_pinned' => 0,
];
$selected_character_org_tag = $selected_character ? sccharacters_resolve_org_tag($selected_character) : '';
$selected_character_has_player_handle = $selected_character ? sccharacters_has_player_handle($selected_character) : false;
$item_has_previous_page = $item_page > 1;
$item_has_next_page = $item_page < $item_total_pages;
$item_query_base_params = [];
if ($selected_character) {
$item_query_base_params = [
'character' => (int) $selected_character['cl_sccharacter_id'],
'item_panel' => 1,
'item_source' => $item_source,
];
if ($item_search !== '') {
$item_query_base_params['item_search'] = $item_search;
}
if ($item_per_page !== 50) {
$item_query_base_params['item_per_page'] = $item_per_page;
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PERSONNAGES | 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-soft: rgba(162, 155, 120, 0.18);
--primary-border: rgba(162, 155, 120, 0.34);
--bg-dark: #080a0f;
--card-bg: rgba(16, 20, 29, 0.86);
--card-bg-strong: rgba(20, 25, 37, 0.95);
--text-main: #ece9df;
--text-soft: rgba(236, 233, 223, 0.74);
--danger: #ff6b6b;
--success: #49d17d;
}
@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, #1d2234 0%, #0a0d13 55%, #050608 100%);
color: var(--text-main);
font-family: 'Electrolize', sans-serif;
}
.admin-layout {
display: flex;
flex-direction: column;
max-width: 1480px;
margin: 0 auto;
padding: 2rem;
gap: 1.5rem;
}
.admin-topbar,
.panel,
.character-card,
.equip-card,
.search-result,
.empty-state {
background: var(--card-bg);
border: 1px solid var(--primary-border);
border-radius: 18px;
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.28);
backdrop-filter: blur(10px);
}
.admin-topbar {
padding: 1.5rem 2rem;
display: flex;
gap: 1rem;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.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.65rem 1.2rem;
border: 1px solid var(--primary-border);
background: rgba(255, 255, 255, 0.02);
color: #fff;
font-family: 'Electrolize', sans-serif;
font-size: 0.88rem;
text-transform: uppercase;
cursor: pointer;
transition: all 0.25s ease;
border-radius: 10px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 44px;
}
.btn-modern:hover {
background: rgba(106, 208, 255, 0.16);
border-color: rgba(106, 208, 255, 0.65);
box-shadow: 0 0 16px rgba(106, 208, 255, 0.18);
transform: translateY(-1px);
}
.btn-modern.danger {
border-color: rgba(255, 123, 123, 0.35);
color: #ffb3b3;
}
.btn-modern.danger:hover {
background: rgba(255, 107, 107, 0.16);
border-color: rgba(255, 107, 107, 0.6);
box-shadow: 0 0 16px rgba(255, 107, 107, 0.2);
}
.flash {
padding: 1rem 1.1rem;
border-radius: 14px;
border: 1px solid transparent;
font-size: 0.95rem;
}
.flash.success { background: rgba(73, 209, 125, 0.14); border-color: rgba(73, 209, 125, 0.3); color: #d6ffe4; }
.flash.error { background: rgba(255, 107, 107, 0.12); border-color: rgba(255, 107, 107, 0.28); color: #ffd7d7; }
.main-grid {
display: grid;
grid-template-columns: 390px minmax(0, 1fr);
gap: 1.5rem;
align-items: start;
}
.sidebar-column,
.right-column {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.panel { padding: 1.3rem; }
.panel-soft {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
}
.panel-title {
margin: 0 0 0.35rem;
font-size: 1.05rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--primary);
}
.panel-subtitle {
margin: 0;
color: var(--text-soft);
font-size: 0.9rem;
line-height: 1.45;
}
.list-panel {
padding-bottom: 1rem;
}
.list-panel-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1rem;
}
.accordion-panel {
padding: 0;
overflow: hidden;
}
.accordion-summary {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1.15rem 1.3rem;
cursor: pointer;
}
.accordion-summary::-webkit-details-marker { display: none; }
.accordion-panel[open] .accordion-summary {
border-bottom: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.02);
}
.accordion-indicator {
width: 34px;
height: 34px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
color: var(--primary);
flex: 0 0 34px;
transition: transform 0.2s ease;
}
.accordion-panel[open] .accordion-indicator {
transform: rotate(45deg);
}
.accordion-body {
padding: 1.25rem 1.3rem 1.3rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem;
}
.field,
.field-full { display: flex; flex-direction: column; gap: 0.45rem; }
.field-full { grid-column: 1 / -1; }
label {
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-soft);
}
input[type="text"],
input[type="url"],
textarea,
select {
width: 100%;
border: 1px solid rgba(255,255,255,0.09);
background: rgba(7, 10, 16, 0.72);
color: var(--text-main);
border-radius: 12px;
padding: 0.82rem 0.9rem;
font-family: inherit;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(162, 155, 120, 0.13);
}
textarea { min-height: 110px; resize: vertical; }
.checkbox-row {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.85rem 0.95rem;
border-radius: 12px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.checkbox-row input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
}
.checkbox-row label {
text-transform: none;
letter-spacing: 0;
font-size: 0.92rem;
color: var(--text-main);
cursor: pointer;
}
.btn-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.btn,
.btn-link {
appearance: none;
border: 0;
border-radius: 12px;
padding: 0.85rem 1rem;
font-family: inherit;
font-size: 0.92rem;
text-decoration: none;
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
}
.btn:hover,
.btn-link:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(0,0,0,0.22);
}
.btn-small,
.btn-link.btn-small {
padding: 0.68rem 0.85rem;
font-size: 0.85rem;
}
.btn-primary { background: linear-gradient(135deg, #d2c6a0, #93875f); color: #0d1017; }
.btn-secondary { background: rgba(255,255,255,0.07); color: var(--text-main); border: 1px solid rgba(255,255,255,0.08); }
.btn-danger { background: rgba(255, 107, 107, 0.14); color: #ffe7e7; border: 1px solid rgba(255, 107, 107, 0.28); }
.characters-stack {
display: flex;
flex-direction: column;
gap: 0.8rem;
max-height: calc(100vh - 360px);
overflow: auto;
padding-right: 0.15rem;
}
.character-card {
display: grid;
grid-template-columns: 56px minmax(0, 1fr);
gap: 0.85rem;
padding: 0.85rem;
color: inherit;
text-decoration: none;
border-color: rgba(255,255,255,0.08);
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.character-card:hover {
transform: translateY(-1px);
border-color: rgba(162, 155, 120, 0.45);
background: rgba(24, 30, 42, 0.94);
}
.character-card.is-active {
border-color: rgba(162, 155, 120, 0.6);
background: rgba(27, 32, 45, 0.96);
box-shadow: 0 0 0 1px rgba(162, 155, 120, 0.18), 0 18px 45px rgba(0, 0, 0, 0.28);
}
.character-avatar,
.character-avatar-fallback {
width: 56px;
height: 56px;
border-radius: 16px;
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.1);
}
.character-avatar-fallback,
.character-hero-fallback,
.equip-thumb-fallback {
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
}
.character-card-body,
.equip-content { min-width: 0; }
.character-card-body h3,
.equip-title,
.character-hero-name {
margin: 0;
}
.character-card-title,
.hero-name-row {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.character-card-body h3 {
font-size: 0.98rem;
line-height: 1.25;
}
.pin-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
border-radius: 999px;
border: 1px solid rgba(162, 155, 120, 0.34);
background: rgba(162, 155, 120, 0.14);
color: #f8f3e1;
font-size: 0.95rem;
line-height: 1;
flex: 0 0 auto;
}
.pin-indicator-input {
width: 2rem;
height: 2rem;
border-radius: 999px;
border: 1px solid rgba(162, 155, 120, 0.25);
background: rgba(162, 155, 120, 0.1);
color: #f8f3e1;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
line-height: 1;
flex: 0 0 auto;
}
.muted {
color: var(--text-soft);
font-size: 0.88rem;
line-height: 1.45;
}
.badge-row,
.hero-meta,
.equip-meta {
display: flex;
gap: 0.45rem;
flex-wrap: wrap;
}
.hero-meta-secondary {
margin-top: 0.7rem;
}
.identity-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.7rem;
}
.identity-item {
display: flex;
flex-direction: column;
gap: 0.22rem;
padding: 0.72rem 0.82rem;
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.95rem;
font-weight: 600;
color: #f6f7fb;
line-height: 1.35;
word-break: break-word;
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.32rem 0.56rem;
border-radius: 999px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
font-size: 0.77rem;
color: #f2f2f2;
}
.badge-primary {
background: rgba(162, 155, 120, 0.18);
border-color: rgba(162, 155, 120, 0.34);
color: #f8f3e1;
}
.badge-muted { color: var(--text-soft); }
.badge-success {
background: rgba(73, 209, 125, 0.12);
border-color: rgba(73, 209, 125, 0.3);
color: #d6ffe4;
}
.character-hero {
display: grid;
grid-template-columns: 110px minmax(0, 1fr);
gap: 1.2rem;
align-items: start;
}
.character-hero-avatar,
.character-hero-fallback {
width: 110px;
height: 110px;
border-radius: 24px;
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.1);
}
.character-hero-fallback {
font-size: 2rem;
}
.character-hero-name {
font-size: 1.5rem;
margin-bottom: 0.55rem;
}
.hero-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 0.8rem;
}
.share-box {
margin-top: 1rem;
padding: 1rem;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
}
.share-box input {
margin-top: 0.65rem;
font-size: 0.88rem;
}
.edit-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem;
}
.equipment-sections {
display: flex;
flex-direction: column;
gap: 1rem;
}
.equipment-section {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.equipment-section-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
padding-bottom: 0.35rem;
border-bottom: 1px solid rgba(255,255,255,0.08);
flex-wrap: wrap;
}
.equipment-section-heading-main {
display: inline-flex;
align-items: center;
gap: 0.65rem;
flex-wrap: wrap;
}
.equipment-category-order-form {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.equipment-section-title {
margin: 0;
font-size: 0.92rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--primary);
}
.equipment-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.equip-card {
padding: 1rem;
display: flex;
gap: 0.9rem;
align-items: flex-start;
cursor: grab;
transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.equip-card.dragging {
opacity: 0.45;
cursor: grabbing;
}
.equip-card.drag-over {
border-color: rgba(162, 155, 120, 0.55);
background: rgba(162, 155, 120, 0.09);
}
.equip-thumb,
.equip-thumb-fallback {
width: 64px;
height: 64px;
border-radius: 16px;
flex: 0 0 64px;
object-fit: cover;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
}
.equip-thumb-fallback {
font-size: 1.2rem;
}
.equip-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.6rem;
min-width: 0;
}
.equip-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
.drag-handle {
color: var(--text-soft);
font-size: 1rem;
line-height: 1;
letter-spacing: -0.1em;
user-select: none;
flex: 0 0 auto;
}
.drag-hint {
font-size: 0.8rem;
}
.equipment-actions {
margin-top: auto;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: flex-start;
justify-content: space-between;
}
.equipment-action-buttons {
display: grid;
grid-template-columns: repeat(2, auto);
align-items: start;
gap: 0.55rem;
justify-content: end;
}
.equipment-edit {
width: auto;
margin-top: 0;
}
.equipment-edit[open] {
grid-column: 1 / -1;
width: min(100%, 34rem);
justify-self: end;
}
.equipment-edit-summary,
.equipment-icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.45rem;
min-width: 2.45rem;
height: 2.45rem;
min-height: 2.45rem;
padding: 0;
cursor: pointer;
list-style: none;
line-height: 1;
}
.equipment-edit-summary::-webkit-details-marker {
display: none;
}
.equipment-edit-form {
margin-top: 0.85rem;
padding: 0.95rem;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.04);
display: grid;
gap: 0.85rem;
width: min(100%, 34rem);
}
.equipment-edit-grid {
display: grid;
grid-template-columns: minmax(110px, 135px) minmax(0, 1fr);
gap: 0.75rem;
align-items: start;
}
.equipment-edit-grid textarea {
grid-column: 1 / -1;
min-height: 96px;
resize: vertical;
}
.item-attach-meta textarea {
grid-column: 1 / -1;
min-height: 72px;
resize: vertical;
}
.equipment-edit-form input,
.equipment-edit-form select,
.equipment-edit-form textarea,
.item-attach-meta input,
.item-attach-meta select,
.item-attach-meta textarea {
width: 100%;
}
.equipment-edit-form-actions {
display: flex;
justify-content: flex-end;
}
.equipment-reorder-form {
display: none;
}
.order-btn {
min-width: 2.2rem;
padding-inline: 0.55rem;
}
.equip-title {
font-size: 1rem;
line-height: 1.3;
margin: 0;
}
.equip-stats {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
align-items: stretch;
margin-top: 0.15rem;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.equip-stat {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0.48rem 0.78rem;
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;
letter-spacing: 0.01em;
color: #dcffe9;
}
.search-toolbar {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.item-search-form {
display: grid;
gap: 0.9rem;
}
.search-top-row {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.search-controls-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 170px auto;
gap: 0.75rem;
align-items: center;
}
.source-switch {
display: inline-grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 0.28rem;
border-radius: 14px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
gap: 0.25rem;
width: fit-content;
}
.source-switch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.source-switch label {
min-width: 155px;
min-height: 46px;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.7rem 0.9rem;
border-radius: 10px;
cursor: pointer;
font-family: inherit;
font-size: 0.82rem;
font-weight: 400;
line-height: 1.2;
letter-spacing: 0.05em;
text-transform: uppercase;
white-space: nowrap;
color: var(--text-soft);
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
border: 1px solid transparent;
}
.source-switch label:hover {
color: var(--text-main);
}
.source-switch input:checked + label {
background: rgba(162, 155, 120, 0.16);
border-color: rgba(162, 155, 120, 0.32);
color: var(--text-main);
}
.search-input-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
}
.search-results {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
}
.search-results-summary,
.pagination-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 1rem;
}
.search-result {
padding: 0.95rem;
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 1rem;
align-items: start;
}
body.show-item-preview .search-result {
grid-template-columns: 96px minmax(0, 1fr) 360px;
}
.search-result strong { display: block; margin-bottom: 0.25rem; }
.search-result-stats {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.05rem;
}
.search-result-stats .equip-stat {
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;
}
.search-result-main {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.55rem;
}
.search-result-preview {
display: none;
align-items: flex-start;
justify-content: center;
}
body.show-item-preview .search-result-preview {
display: flex;
}
.preview-container {
position: relative;
width: 80px;
height: 80px;
cursor: zoom-in;
}
.item-preview,
.item-preview-fallback {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(0,0,0,0.35);
display: block;
}
.item-preview-fallback {
display: grid;
place-items: center;
font-size: 1.2rem;
color: var(--text-soft);
}
.preview-floating {
visibility: hidden;
opacity: 0;
position: absolute;
top: -10px;
left: 92px;
z-index: 20;
padding: 5px;
background: var(--card-bg);
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;
}
.item-attach-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-self: stretch;
align-items: stretch;
}
.item-attach-meta {
display: grid;
grid-template-columns: minmax(96px, 112px) minmax(0, 1fr);
gap: 0.55rem;
align-items: start;
}
.item-attach-actions {
display: flex;
align-self: stretch;
}
.item-add-button {
min-width: 3rem;
min-height: 100%;
height: 100%;
padding: 0.4rem;
font-size: 1.55rem;
line-height: 1;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
}
.search-results-summary {
flex-wrap: wrap;
}
.preview-toggle {
display: inline-flex;
align-items: center;
gap: 0.55rem;
font-size: 0.9rem;
color: var(--text-main);
user-select: none;
cursor: pointer;
}
.preview-toggle input {
width: 1rem;
height: 1rem;
margin: 0;
}
.search-result-range {
font-size: 0.88rem;
color: var(--text-soft);
}
.pagination-links {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
align-items: center;
}
.pagination-label {
font-size: 0.88rem;
color: var(--text-soft);
}
.empty-state {
padding: 1.2rem;
color: var(--text-soft);
text-align: center;
}
.empty-state strong { color: var(--text-main); display: block; margin-bottom: 0.35rem; }
.nav-tabs {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--primary-border);
padding-bottom: 1rem;
flex-wrap: wrap;
}
.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);
}
@media (max-width: 1280px) {
.main-grid {
grid-template-columns: 340px minmax(0, 1fr);
}
.search-results-summary,
.pagination-bar {
flex-direction: column;
align-items: stretch;
}
.search-result,
body.show-item-preview .search-result {
grid-template-columns: 1fr;
}
.search-result-preview {
justify-content: flex-start;
}
.item-attach-form,
.item-attach-meta,
.search-controls-row {
grid-template-columns: 1fr;
}
.item-attach-actions {
justify-content: stretch;
}
.item-add-button {
width: 100%;
min-height: 2.75rem;
height: auto;
}
}
@media (max-width: 1180px) {
.main-grid,
.equipment-grid,
.edit-form-grid {
grid-template-columns: 1fr;
}
.characters-stack {
max-height: none;
}
}
@media (max-width: 720px) {
.admin-layout { padding: 1rem; }
.form-grid,
.character-hero,
.search-input-row,
.source-switch {
grid-template-columns: 1fr;
}
.topbar-actions { width: 100%; justify-content: flex-start; }
.list-panel-header,
.hero-header {
flex-direction: column;
align-items: stretch;
}
.source-switch {
width: 100%;
}
.source-switch label {
min-width: 0;
}
}
</style>
</head>
<body class="sccharacters-page">
<?php echo auth_render_page_access_widget('sccharacters.php', 'Personnages'); ?>
<div class="admin-layout">
<header class="admin-topbar">
<div class="topbar-info">
<h1>R.E.A.C.T. Characters Control</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('sccharacters.php'); ?>
<?php if ($flash_message !== ''): ?>
<div class="flash <?php echo $flash_type === 'success' ? 'success' : 'error'; ?>">
<?php echo htmlspecialchars($flash_message, ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<div class="main-grid">
<aside class="sidebar-column">
<details class="panel accordion-panel" <?php echo $create_panel_open ? 'open' : ''; ?>>
<summary class="accordion-summary">
<div>
<h2 class="panel-title">Créer un personnage</h2>
<p class="panel-subtitle">Le formulaire est replié par défaut pour laisser un maximum de place à ta liste de personnages.</p>
</div>
<span class="accordion-indicator">+</span>
</summary>
<div class="accordion-body">
<form method="post" id="characterCreateForm">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="create_character">
<div class="form-grid">
<div class="field-full">
<label for="newCharacterName">Nom</label>
<input type="text" id="newCharacterName" name="character_name" maxlength="190" required value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field">
<label for="newCharacterRole">Rôle / Classe</label>
<input type="text" id="newCharacterRole" name="character_role" maxlength="190" value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field">
<label for="newCharacterOrgRsiUrl">Page RSI (URL)</label>
<input type="url" id="newCharacterOrgRsiUrl" name="character_org_rsi_url" placeholder="https://robertsspaceindustries.com/en/orgs/REACT" value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_org_rsi_url'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field">
<label for="newCharacterPlayerHandle">Handle</label>
<input type="text" id="newCharacterPlayerHandle" name="character_player_handle" maxlength="190" value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_player_handle'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-full">
<label for="newCharacterAvatar">Avatar (URL dimage)</label>
<input type="url" id="newCharacterAvatar" name="character_avatar_url" placeholder="https://..." value="<?php echo htmlspecialchars((string) $create_character['cl_sccharacter_avatar_url'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-full">
<label for="newCharacterDescription">Description</label>
<textarea id="newCharacterDescription" name="character_description"><?php echo htmlspecialchars((string) $create_character['cl_sccharacter_description'], ENT_QUOTES, 'UTF-8'); ?></textarea>
</div>
<div class="field-full">
<label for="newCharacterNotes">Notes privées</label>
<textarea id="newCharacterNotes" name="character_notes"><?php echo htmlspecialchars((string) $create_character['cl_sccharacter_notes'], ENT_QUOTES, 'UTF-8'); ?></textarea>
</div>
<div class="field-full">
<div class="checkbox-row">
<input type="checkbox" id="newCharacterShareEnabled" name="character_share_enabled" value="1" <?php echo !empty($create_character['cl_sccharacter_share_enabled']) ? 'checked' : ''; ?>>
<label for="newCharacterShareEnabled">Activer laccès public via le lien partagé</label>
</div>
</div>
<div class="field-full">
<div class="checkbox-row">
<span class="pin-indicator-input" aria-hidden="true">📌</span>
<input type="checkbox" id="newCharacterPinned" name="character_is_pinned" value="1" <?php echo !empty($create_character['cl_sccharacter_is_pinned']) ? 'checked' : ''; ?>>
<label for="newCharacterPinned">Toujours afficher ce personnage en haut de la liste</label>
</div>
</div>
</div>
<div class="btn-row">
<button type="submit" class="btn btn-primary">Créer le personnage</button>
</div>
</form>
</div>
</details>
<div class="panel list-panel">
<div class="list-panel-header">
<div>
<h2 class="panel-title">Liste des personnages</h2>
<p class="panel-subtitle"><?php echo count($characters); ?> personnage(s). Sélectionne une fiche pour voir son détail, la modifier et lui attribuer son équipement.</p>
</div>
</div>
<div class="characters-stack">
<?php if ($characters === []): ?>
<div class="empty-state">
<strong>Aucun personnage pour le moment</strong>
Ouvre le bloc « Créer un personnage » ci-dessus pour commencer.
</div>
<?php else: ?>
<?php foreach ($characters as $character_row): ?>
<?php
$character_id = (int) $character_row['cl_sccharacter_id'];
$is_active = $selected_character && $character_id === (int) $selected_character['cl_sccharacter_id'];
$avatar_url = trim((string) ($character_row['cl_sccharacter_avatar_url'] ?? ''));
$initial = function_exists('mb_substr')
? mb_strtoupper(mb_substr((string) $character_row['cl_sccharacter_name'], 0, 1, 'UTF-8'), 'UTF-8')
: strtoupper(substr((string) $character_row['cl_sccharacter_name'], 0, 1));
$character_row_org_tag = sccharacters_resolve_org_tag($character_row);
$character_row_has_player_handle = sccharacters_has_player_handle($character_row);
$character_link = 'sccharacters.php?character=' . $character_id . '&item_source=' . rawurlencode($item_source);
if ($item_search !== '') {
$character_link .= '&item_search=' . rawurlencode($item_search);
}
?>
<a class="character-card<?php echo $is_active ? ' is-active' : ''; ?>" href="<?php echo htmlspecialchars($character_link, ENT_QUOTES, 'UTF-8'); ?>">
<?php if ($avatar_url !== ''): ?>
<img class="character-avatar" src="<?php echo htmlspecialchars($avatar_url, ENT_QUOTES, 'UTF-8'); ?>" alt="Avatar de <?php echo htmlspecialchars((string) $character_row['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className: 'character-avatar-fallback', textContent: '<?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?>'}));">
<?php else: ?>
<div class="character-avatar-fallback"><?php echo htmlspecialchars($initial, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<div class="character-card-body">
<div class="character-card-title">
<h3><?php echo htmlspecialchars((string) $character_row['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h3>
<?php if (!empty($character_row['cl_sccharacter_is_pinned'])): ?>
<span class="pin-indicator" title="Personnage épinglé" aria-label="Personnage épinglé">📌</span>
<?php endif; ?>
</div>
<div class="badge-row" style="margin:0.42rem 0 0.52rem;">
<?php if (trim((string) $character_row['cl_sccharacter_role']) !== ''): ?>
<span class="badge badge-primary"><?php echo htmlspecialchars((string) $character_row['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<?php if ($character_row_org_tag !== ''): ?>
<span class="badge">Org: <?php echo htmlspecialchars($character_row_org_tag, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<?php if ($character_row_has_player_handle): ?>
<span class="badge">Handle: <?php echo htmlspecialchars((string) $character_row['cl_sccharacter_player_handle'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<span class="badge badge-muted"><?php echo (int) $character_row['cl_sccharacter_item_count']; ?> objet(s)</span>
<?php if (!empty($character_row['cl_sccharacter_share_enabled'])): ?>
<span class="badge badge-success">Partage actif</span>
<?php endif; ?>
</div>
<div class="muted">
<?php echo htmlspecialchars(trim((string) $character_row['cl_sccharacter_description']) !== '' ? sccharacters_excerpt((string) $character_row['cl_sccharacter_description'], 110) : 'Aucune description pour le moment.', ENT_QUOTES, 'UTF-8'); ?>
</div>
</div>
</a>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</aside>
<section class="right-column">
<?php if ($selected_character): ?>
<?php
$hero_avatar = trim((string) ($selected_character['cl_sccharacter_avatar_url'] ?? ''));
$hero_initial = function_exists('mb_substr')
? mb_strtoupper(mb_substr((string) $selected_character['cl_sccharacter_name'], 0, 1, 'UTF-8'), 'UTF-8')
: strtoupper(substr((string) $selected_character['cl_sccharacter_name'], 0, 1));
?>
<div class="panel">
<div class="character-hero">
<?php if ($hero_avatar !== ''): ?>
<img class="character-hero-avatar" src="<?php echo htmlspecialchars($hero_avatar, ENT_QUOTES, 'UTF-8'); ?>" alt="Avatar de <?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className: 'character-hero-fallback', textContent: '<?php echo htmlspecialchars($hero_initial, ENT_QUOTES, 'UTF-8'); ?>'}));">
<?php else: ?>
<div class="character-hero-fallback"><?php echo htmlspecialchars($hero_initial, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<div>
<div class="hero-header">
<div>
<div class="hero-name-row">
<h2 class="character-hero-name"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?></h2>
<?php if (!empty($selected_character['cl_sccharacter_is_pinned'])): ?>
<span class="pin-indicator" title="Personnage épinglé" aria-label="Personnage épinglé">📌</span>
<?php endif; ?>
</div>
<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) $selected_character['cl_sccharacter_role']) !== '' ? (string) $selected_character['cl_sccharacter_role'] : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="identity-item">
<span class="identity-label">Tag organisation</span>
<span class="identity-value"><?php echo htmlspecialchars($selected_character_org_tag !== '' ? $selected_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) ($selected_character['cl_sccharacter_player_handle'] ?? '')) !== '' ? (string) $selected_character['cl_sccharacter_player_handle'] : '—', ENT_QUOTES, 'UTF-8'); ?></span>
</div>
</div>
<div class="hero-meta hero-meta-secondary">
<span class="badge badge-muted"><?php echo count($selected_character_items); ?> équipement(s)</span>
</div>
</div>
<span class="badge <?php echo !empty($selected_character['cl_sccharacter_share_enabled']) ? 'badge-success' : 'badge-muted'; ?>">
<?php echo !empty($selected_character['cl_sccharacter_share_enabled']) ? 'Lien public actif' : 'Lien public désactivé'; ?>
</span>
</div>
<p class="muted">
<?php echo nl2br(htmlspecialchars(trim((string) $selected_character['cl_sccharacter_description']) !== '' ? (string) $selected_character['cl_sccharacter_description'] : 'Aucune description publique renseignée pour ce personnage.', ENT_QUOTES, 'UTF-8')); ?>
</p>
<div class="share-box">
<div class="badge-row">
<span class="badge badge-muted">Partage par lien uniquement</span>
<span class="badge badge-muted">Page publique dédiée</span>
</div>
<input type="text" id="shareLinkField" readonly value="<?php echo htmlspecialchars(sccharacters_share_url((string) $selected_character['cl_sccharacter_share_token']), ENT_QUOTES, 'UTF-8'); ?>">
<div class="btn-row" style="margin-top:0.75rem;">
<button type="button" class="btn btn-secondary btn-small" onclick="copyShareLink()">Copier le lien</button>
<form method="post" style="display:inline-flex;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="regenerate_share_token">
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<button type="submit" class="btn btn-secondary btn-small">Régénérer</button>
</form>
</div>
</div>
</div>
</div>
</div>
<details class="panel accordion-panel">
<summary class="accordion-summary">
<div>
<h2 class="panel-title">Modifier la fiche</h2>
<p class="panel-subtitle">Bloc repliable pour garder la fiche visible sans laisser le formulaire ouvert en permanence.</p>
</div>
<span class="accordion-indicator">+</span>
</summary>
<div class="accordion-body">
<form method="post" id="characterEditForm">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="update_character">
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<input type="hidden" name="category_order_state" id="characterCategoryOrderState" value="<?php echo htmlspecialchars($selected_character_visible_category_order_state, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_order_state" id="characterItemOrderState" value="<?php echo htmlspecialchars($selected_character_item_order_state_json, ENT_QUOTES, 'UTF-8'); ?>">
<div class="edit-form-grid">
<div class="field-full">
<label for="editCharacterName">Nom</label>
<input type="text" id="editCharacterName" name="character_name" maxlength="190" required value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field">
<label for="editCharacterRole">Rôle / Classe</label>
<input type="text" id="editCharacterRole" name="character_role" maxlength="190" value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_role'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field">
<label for="editCharacterOrgRsiUrl">Page RSI (URL)</label>
<input type="url" id="editCharacterOrgRsiUrl" name="character_org_rsi_url" placeholder="https://robertsspaceindustries.com/en/orgs/REACT" value="<?php echo htmlspecialchars((string) ($selected_character['cl_sccharacter_org_rsi_url'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field">
<label for="editCharacterPlayerHandle">Handle</label>
<input type="text" id="editCharacterPlayerHandle" name="character_player_handle" maxlength="190" value="<?php echo htmlspecialchars((string) ($selected_character['cl_sccharacter_player_handle'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-full">
<label for="editCharacterAvatar">Avatar (URL dimage)</label>
<input type="url" id="editCharacterAvatar" name="character_avatar_url" placeholder="https://..." value="<?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_avatar_url'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="field-full">
<label for="editCharacterDescription">Description</label>
<textarea id="editCharacterDescription" name="character_description"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_description'], ENT_QUOTES, 'UTF-8'); ?></textarea>
</div>
<div class="field-full">
<label for="editCharacterNotes">Notes privées</label>
<textarea id="editCharacterNotes" name="character_notes"><?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_notes'], ENT_QUOTES, 'UTF-8'); ?></textarea>
</div>
<div class="field-full">
<div class="checkbox-row">
<input type="checkbox" id="editCharacterShareEnabled" name="character_share_enabled" value="1" <?php echo !empty($selected_character['cl_sccharacter_share_enabled']) ? 'checked' : ''; ?>>
<label for="editCharacterShareEnabled">Activer laccès public via le lien partagé</label>
</div>
</div>
<div class="field-full">
<div class="checkbox-row">
<span class="pin-indicator-input" aria-hidden="true">📌</span>
<input type="checkbox" id="editCharacterPinned" name="character_is_pinned" value="1" <?php echo !empty($selected_character['cl_sccharacter_is_pinned']) ? 'checked' : ''; ?>>
<label for="editCharacterPinned">Toujours afficher ce personnage en haut de la liste</label>
</div>
</div>
</div>
<div class="btn-row">
<button type="submit" class="btn btn-primary">Enregistrer</button>
<span class="muted order-state-indicator" id="characterOrderStateIndicator" hidden>Des changements dordre sont en attente denregistrement.</span>
</div>
</form>
<div class="btn-row">
<form method="post" style="display:inline-flex;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_character">
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<button type="submit" class="btn btn-danger" onclick="return confirm('Supprimer ce personnage et son équipement ?');">Supprimer le personnage</button>
</form>
</div>
</div>
</details>
<details class="panel accordion-panel" open>
<summary class="accordion-summary">
<div>
<h2 class="panel-title">Équipement attribué</h2>
<p class="panel-subtitle">Bloc repliable pour consulter rapidement léquipement et le refermer quand tu veux dégager la vue.</p>
</div>
<span class="accordion-indicator">+</span>
</summary>
<div class="accordion-body">
<?php if ($selected_character_items === []): ?>
<div class="empty-state">
<strong>Aucun équipement attribué</strong>
Utilise le module de recherche plus bas pour ajouter un objet de la base ou un objet personnalisé.
</div>
<?php else: ?>
<div class="equipment-order-toolbar">
<span class="muted">Tu peux réorganiser librement les catégories et les objets ci-dessous, puis enregistrer quand tu veux.</span>
<button type="submit" form="characterEditForm" class="btn btn-primary btn-small">Enregistrer lordre</button>
</div>
<div class="equipment-sections" id="characterEquipmentSections">
<?php foreach ($selected_character_items_by_category as $category_key => $category_items): ?>
<?php $category_order_meta = $selected_character_category_order_meta[$category_key] ?? ['is_first' => false, 'is_last' => false]; ?>
<section class="equipment-section" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>">
<div class="equipment-section-heading">
<div class="equipment-section-heading-main">
<h3 class="equipment-section-title"><?php echo htmlspecialchars(sccharacters_item_category_label((string) $category_key), ENT_QUOTES, 'UTF-8'); ?></h3>
<span class="badge badge-muted"><?php echo count($category_items); ?> objet(s)</span>
</div>
<div class="equipment-category-order-controls" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>">
<button type="button" class="btn btn-secondary btn-small order-btn category-order-btn" data-move-direction="up" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>" title="Afficher cette catégorie plus haut" aria-label="Afficher cette catégorie plus haut" <?php echo $category_order_meta['is_first'] ? 'disabled' : ''; ?>>↑</button>
<button type="button" class="btn btn-secondary btn-small order-btn category-order-btn" data-move-direction="down" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>" title="Afficher cette catégorie plus bas" aria-label="Afficher cette catégorie plus bas" <?php echo $category_order_meta['is_last'] ? 'disabled' : ''; ?>>↓</button>
</div>
</div>
<div class="equipment-grid equipment-grid-sortable" data-category-key="<?php echo htmlspecialchars((string) $category_key, ENT_QUOTES, 'UTF-8'); ?>">
<?php foreach ($category_items as $character_item_row): ?>
<?php
$is_custom = ($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
$item_name = $is_custom
? (string) ($character_item_row['cl_sccharacteritem_custom_name'] ?? 'Objet personnalisé indisponible')
: (string) ($character_item_row['cl_sccharacteritem_base_name'] ?? 'Objet indisponible');
$item_type = $is_custom
? (string) ($character_item_row['cl_sccharacteritem_custom_type'] ?? '')
: (string) ($character_item_row['cl_sccharacteritem_base_type'] ?? '');
$item_subtype = $is_custom
? (string) ($character_item_row['cl_sccharacteritem_custom_subtype'] ?? '')
: (string) ($character_item_row['cl_sccharacteritem_base_subtype'] ?? '');
$item_uuid = $is_custom
? (string) ($character_item_row['cl_sccharacteritem_custom_uuid'] ?? '')
: (string) ($character_item_row['cl_sccharacteritem_base_uuid'] ?? '');
$item_category = sccharacters_resolve_item_category(
(string) ($character_item_row['cl_sccharacteritem_slot'] ?? ''),
$item_type,
$item_subtype
);
$item_quantity = sccharacters_normalize_item_quantity($character_item_row['cl_sccharacteritem_quantity'] ?? null);
$item_title = $item_quantity !== null ? $item_quantity . 'x ' . $item_name : $item_name;
$item_note = trim((string) ($character_item_row['cl_sccharacteritem_note'] ?? ''));
$item_stats = [];
if ($is_custom) {
$item_stats = $custom_stats_by_itemcustom[(int) ($character_item_row['cl_sccharacteritem_scitemcustom_id'] ?? 0)] ?? [];
}
?>
<article class="equip-card" draggable="true" data-item-id="<?php echo (int) $character_item_row['cl_sccharacteritem_id']; ?>">
<?php if ($item_uuid !== ''): ?>
<img class="equip-thumb" src="https://cstone.space/uifimages/<?php echo htmlspecialchars($item_uuid, ENT_QUOTES, 'UTF-8'); ?>.png" alt="Aperçu de <?php echo htmlspecialchars($item_name, ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className: 'equip-thumb-fallback', textContent: '◈'}));">
<?php else: ?>
<div class="equip-thumb-fallback">◈</div>
<?php endif; ?>
<div class="equip-content">
<div>
<div class="equip-title-row">
<h3 class="equip-title"><?php echo htmlspecialchars($item_title, ENT_QUOTES, 'UTF-8'); ?></h3>
<span class="drag-handle" title="Glisser pour changer lordre" aria-hidden="true">⋮⋮</span>
</div>
<div class="equip-meta" style="margin-top:0.45rem;">
<span class="badge <?php echo $is_custom ? 'badge-primary' : 'badge-muted'; ?>"><?php echo $is_custom ? 'Objet perso.' : 'Base dobjets'; ?></span>
<span class="badge"><?php echo htmlspecialchars(sccharacters_item_category_label($item_category), ENT_QUOTES, 'UTF-8'); ?></span>
<?php if ($item_type !== ''): ?><span class="badge badge-muted"><?php echo htmlspecialchars($item_type, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
<?php if ($item_subtype !== ''): ?><span class="badge badge-muted"><?php echo htmlspecialchars($item_subtype, ENT_QUOTES, 'UTF-8'); ?></span><?php endif; ?>
</div>
</div>
<?php if ($item_stats !== []): ?>
<div class="equip-stats">
<?php foreach ($item_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="equip-stat"><?php echo htmlspecialchars((string) $stat_row['cl_scstatsitem_name'], ENT_QUOTES, 'UTF-8'); ?> : <?php echo htmlspecialchars($sign . $value . ' ' . (string) $stat_row['cl_scstatsitem_unit'], ENT_QUOTES, 'UTF-8'); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($item_note !== ''): ?>
<div class="muted"><?php echo nl2br(htmlspecialchars($item_note, ENT_QUOTES, 'UTF-8')); ?></div>
<?php endif; ?>
<div class="equipment-actions">
<span class="drag-hint muted">Glisser-déposer pour réordonner dans cette catégorie</span>
<div class="equipment-action-buttons">
<details class="equipment-edit">
<summary class="btn btn-secondary btn-small equipment-edit-summary" title="Modifier lobjet" aria-label="Modifier lobjet"><span aria-hidden="true">✎</span></summary>
<form method="post" class="equipment-edit-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="update_character_item">
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<input type="hidden" name="character_item_id" value="<?php echo (int) $character_item_row['cl_sccharacteritem_id']; ?>">
<input type="hidden" name="item_source_context" value="<?php echo htmlspecialchars($item_source, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_search_context" value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_page_context" value="<?php echo (int) $item_page; ?>">
<input type="hidden" name="item_per_page_context" value="<?php echo (int) $item_per_page; ?>">
<input type="hidden" name="item_panel_context" value="<?php echo ($item_search !== '' || $item_panel_open) ? '1' : '0'; ?>">
<div class="equipment-edit-grid">
<input type="number" name="item_quantity" min="1" step="1" inputmode="numeric" placeholder="Quantité" value="<?php echo $item_quantity !== null ? (int) $item_quantity : ''; ?>" aria-label="Quantité de l'objet">
<select name="item_slot" aria-label="Catégorie de l'objet">
<?php foreach ($item_category_options as $category_value => $category_label): ?>
<option value="<?php echo htmlspecialchars($category_value, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $item_category === $category_value ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($category_label, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<textarea name="item_note" class="item-note-field" rows="4" placeholder="Note optionnelle"><?php echo htmlspecialchars($item_note, ENT_QUOTES, 'UTF-8'); ?></textarea>
</div>
<div class="equipment-edit-form-actions">
<button type="submit" class="btn btn-primary btn-small">Enregistrer</button>
</div>
</form>
</details>
<form method="post" style="display:inline-flex;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_character_item">
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<input type="hidden" name="character_item_id" value="<?php echo (int) $character_item_row['cl_sccharacteritem_id']; ?>">
<input type="hidden" name="item_source_context" value="<?php echo htmlspecialchars($item_source, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_search_context" value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_page_context" value="<?php echo (int) $item_page; ?>">
<input type="hidden" name="item_per_page_context" value="<?php echo (int) $item_per_page; ?>">
<input type="hidden" name="item_panel_context" value="<?php echo ($item_search !== '' || $item_panel_open) ? '1' : '0'; ?>">
<button type="submit" class="btn btn-danger btn-small equipment-icon-button" title="Retirer lobjet" aria-label="Retirer lobjet" onclick="return confirm('Retirer cet objet du personnage ?');"><span aria-hidden="true">🗑</span></button>
</form>
</div>
</div>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</details>
<details class="panel accordion-panel" <?php echo ($item_search !== '' || $item_panel_open) ? 'open' : ''; ?>>
<summary class="accordion-summary">
<div>
<h2 class="panel-title">Ajouter des objets</h2>
<p class="panel-subtitle">Recherche repliable : tu louvres quand tu équipes le personnage, tu la refermes dès que cest fait.</p>
</div>
<span class="accordion-indicator">+</span>
</summary>
<div class="accordion-body">
<div class="search-toolbar">
<form method="get" class="item-search-form" id="itemSearchForm" data-character-id="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<input type="hidden" name="character" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<input type="hidden" name="item_panel" value="1">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<div class="search-top-row">
<div class="source-switch" role="radiogroup" aria-label="Source de recherche dobjet">
<input type="radio" name="item_source" id="itemSourceBase" value="base" <?php echo $item_source === 'base' ? 'checked' : ''; ?>>
<label for="itemSourceBase">Base dobjets</label>
<input type="radio" name="item_source" id="itemSourceCustom" value="custom" <?php echo $item_source === 'custom' ? 'checked' : ''; ?>>
<label for="itemSourceCustom">Objets perso.</label>
</div>
<label class="preview-toggle" for="itemPreviewToggle">
<input type="checkbox" id="itemPreviewToggle">
<span>Afficher laperçu</span>
</label>
</div>
<div class="search-controls-row">
<input type="text" name="item_search" placeholder="Nom, type, subtype, UUID..." value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
<select name="item_per_page" aria-label="Nombre de résultats affichés">
<?php foreach (sccharacters_allowed_item_per_page() as $per_page_option): ?>
<option value="<?php echo (int) $per_page_option; ?>" <?php echo $item_per_page === $per_page_option ? 'selected' : ''; ?>>
<?php echo (int) $per_page_option; ?> résultats / page
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-primary btn-small">Rechercher</button>
</div>
</form>
</div>
<div class="search-results">
<?php if ($item_results === []): ?>
<div class="empty-state">
<strong><?php echo $item_source === 'custom' ? 'Aucun objet personnalisé trouvé' : 'Aucun objet trouvé'; ?></strong>
<?php if ($item_source === 'custom'): ?>
<?php if ($item_search !== ''): ?>
Aucun objet personnalisé ne correspond à « <?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?> ».
<?php else: ?>
Commence par créer des objets dans longlet « Objets perso. » pour pouvoir les attribuer à tes personnages.
<?php endif; ?>
<?php else: ?>
<?php if ($item_search !== ''): ?>
Aucun objet de la base ne correspond à « <?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?> ».
<?php else: ?>
Lance une recherche ou parcours le catalogue page par page avec le sélecteur de résultats.
<?php endif; ?>
<?php endif; ?>
</div>
<?php else: ?>
<div class="search-results-summary panel panel-soft">
<div>
<strong>Résultats de recherche</strong>
<div class="search-result-range">
<?php echo (int) $item_result_offset_start; ?><?php echo (int) $item_result_offset_end; ?> sur <?php echo (int) $item_total_results; ?> résultat(s)
· page <?php echo (int) $item_page; ?> / <?php echo (int) $item_total_pages; ?>
</div>
</div>
<div class="muted">Le bouton <strong>+</strong> ajoute lobjet immédiatement au personnage sans casser ta recherche en cours.</div>
</div>
<?php foreach ($item_results as $item_result_row): ?>
<?php
$result_is_custom = $item_source === 'custom';
$result_name = (string) $item_result_row['cl_scobjs_name'];
$result_type = trim(((string) $item_result_row['cl_scobjs_type']) . ' · ' . ((string) $item_result_row['cl_scobjs_subtype']), ' ·');
$recommended_category = sccharacters_resolve_item_category(
'',
(string) ($item_result_row['cl_scobjs_type'] ?? ''),
(string) ($item_result_row['cl_scobjs_subtype'] ?? '')
);
$result_id = $result_is_custom
? (int) ($item_result_row['cl_scitemcustom_id'] ?? 0)
: (int) ($item_result_row['cl_scobjs_id'] ?? 0);
$item_action = $result_is_custom ? 'add_custom_item' : 'add_base_item';
$item_id_field = $result_is_custom ? 'custom_item_id' : 'base_obj_id';
$item_uuid = (string) ($item_result_row['cl_scobjs_uuid'] ?? '');
$item_image_url = $item_uuid !== '' ? 'https://cstone.space/uifimages/' . rawurlencode($item_uuid) . '.png' : '';
$result_stats = $result_is_custom ? ($item_result_custom_stats[$result_id] ?? []) : [];
?>
<article class="search-result panel panel-soft">
<div class="search-result-preview">
<?php if ($item_image_url !== ''): ?>
<div class="preview-container">
<img src="<?php echo htmlspecialchars($item_image_url, ENT_QUOTES, 'UTF-8'); ?>" class="item-preview" alt="Aperçu de <?php echo htmlspecialchars($result_name, ENT_QUOTES, 'UTF-8'); ?>" loading="lazy" onerror="this.replaceWith(Object.assign(document.createElement('div'), {className:'item-preview-fallback', textContent:'◈'}));">
<div class="preview-floating" aria-hidden="true">
<img src="<?php echo htmlspecialchars($item_image_url, ENT_QUOTES, 'UTF-8'); ?>" alt="" loading="lazy" onerror="this.closest('.preview-floating').remove();">
</div>
</div>
<?php else: ?>
<div class="item-preview-fallback">◈</div>
<?php endif; ?>
</div>
<div class="search-result-main">
<div>
<strong><?php echo htmlspecialchars($result_name, ENT_QUOTES, 'UTF-8'); ?></strong>
<div class="muted"><?php echo htmlspecialchars($result_type !== '' ? $result_type : 'Type non renseigné', ENT_QUOTES, 'UTF-8'); ?></div>
</div>
<div class="badge-row">
<span class="badge <?php echo $result_is_custom ? 'badge-primary' : 'badge-muted'; ?>"><?php echo $result_is_custom ? 'Objet perso.' : 'Base dobjets'; ?></span>
<?php if ($item_uuid !== ''): ?>
<span class="badge badge-muted">UUID : <?php echo htmlspecialchars($item_uuid, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
<?php if ($result_is_custom): ?>
<span class="badge badge-muted"><?php echo (int) $item_result_row['cl_scitemcustom_stat_count']; ?> stat(s)</span>
<?php endif; ?>
</div>
<?php if ($result_is_custom && $result_stats !== []): ?>
<div class="equip-stats search-result-stats">
<?php foreach ($result_stats as $stat_row): ?>
<span class="equip-stat"><?php echo htmlspecialchars(sccharacters_custom_stat_preview($stat_row), ENT_QUOTES, 'UTF-8'); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<form method="post" class="item-attach-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="<?php echo htmlspecialchars($item_action, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="character_id" value="<?php echo (int) $selected_character['cl_sccharacter_id']; ?>">
<input type="hidden" name="<?php echo htmlspecialchars($item_id_field, ENT_QUOTES, 'UTF-8'); ?>" value="<?php echo (int) $result_id; ?>">
<input type="hidden" name="item_source_context" value="<?php echo htmlspecialchars($item_source, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_search_context" value="<?php echo htmlspecialchars($item_search, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="item_page_context" value="<?php echo (int) $item_page; ?>">
<input type="hidden" name="item_per_page_context" value="<?php echo (int) $item_per_page; ?>">
<div class="item-attach-meta">
<input type="number" name="item_quantity" min="1" step="1" inputmode="numeric" placeholder="Quantité" aria-label="Quantité de l'objet">
<select name="item_slot" aria-label="Catégorie de l'objet">
<?php foreach ($item_category_options as $category_value => $category_label): ?>
<option value="<?php echo htmlspecialchars($category_value, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $recommended_category === $category_value ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($category_label, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<textarea name="item_note" class="item-note-field" rows="2" placeholder="Note optionnelle"></textarea>
</div>
<div class="item-attach-actions">
<button type="submit" class="btn btn-primary item-add-button" title="Ajouter immédiatement" aria-label="Ajouter <?php echo htmlspecialchars($result_name, ENT_QUOTES, 'UTF-8'); ?> au personnage">+</button>
</div>
</form>
</article>
<?php endforeach; ?>
<div class="pagination-bar panel panel-soft">
<div class="pagination-label">Navigation dans les résultats</div>
<div class="pagination-links">
<?php
$prev_params = $item_query_base_params;
$next_params = $item_query_base_params;
if ($item_has_previous_page) {
if (($item_page - 1) > 1) {
$prev_params['item_page'] = $item_page - 1;
}
}
if ($item_has_next_page) {
$next_params['item_page'] = $item_page + 1;
}
?>
<?php if ($item_has_previous_page): ?>
<a class="btn-link btn-small btn-secondary" href="sccharacters.php?<?php echo htmlspecialchars(http_build_query($prev_params), ENT_QUOTES, 'UTF-8'); ?>">← Page précédente</a>
<?php endif; ?>
<?php if ($item_has_next_page): ?>
<a class="btn-link btn-small btn-secondary" href="sccharacters.php?<?php echo htmlspecialchars(http_build_query($next_params), ENT_QUOTES, 'UTF-8'); ?>">Page suivante →</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div> </details>
<?php else: ?>
<div class="empty-state">
<strong>Aucun personnage sélectionné</strong>
Crée un personnage via le bloc repliable de gauche, puis sélectionne-le dans la liste pour gérer sa fiche et son équipement.
</div>
<?php endif; ?>
</section>
</div>
<script>
function copyShareLink() {
const input = document.getElementById('shareLinkField');
if (!input) {
return;
}
input.select();
input.setSelectionRange(0, input.value.length);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(input.value).catch(() => document.execCommand('copy'));
} else {
document.execCommand('copy');
}
}
const itemSearchForm = document.getElementById('itemSearchForm');
const itemPreviewToggle = document.getElementById('itemPreviewToggle');
const itemSourceInputs = Array.from(document.querySelectorAll('input[name="item_source"]'));
const previewStorageKey = 'sccharacters-item-preview-v2';
function setPreviewState(enabled) {
document.body.classList.toggle('show-item-preview', enabled);
if (itemPreviewToggle) {
itemPreviewToggle.checked = enabled;
}
}
let previewEnabled = true;
try {
const storedPreviewState = window.localStorage.getItem(previewStorageKey);
previewEnabled = storedPreviewState === null ? true : storedPreviewState === '1';
} catch (error) {
previewEnabled = true;
}
setPreviewState(previewEnabled);
if (itemPreviewToggle) {
itemPreviewToggle.addEventListener('change', () => {
const enabled = itemPreviewToggle.checked;
setPreviewState(enabled);
try {
window.localStorage.setItem(previewStorageKey, enabled ? '1' : '0');
} catch (error) {
// Ignore storage issues silently.
}
});
}
itemSourceInputs.forEach((input) => {
input.addEventListener('change', () => {
if (itemSearchForm) {
itemSearchForm.submit();
}
});
});
function initEquipmentOrdering() {
const editForm = document.getElementById('characterEditForm');
const categoryOrderInput = document.getElementById('characterCategoryOrderState');
const itemOrderInput = document.getElementById('characterItemOrderState');
const orderStateIndicator = document.getElementById('characterOrderStateIndicator');
const equipmentSections = document.getElementById('characterEquipmentSections');
const categoryButtons = Array.from(document.querySelectorAll('.category-order-btn'));
const grids = Array.from(document.querySelectorAll('.equipment-grid-sortable'));
if (!editForm || !categoryOrderInput || !itemOrderInput || !equipmentSections) {
return;
}
const readItemOrderState = () => {
try {
const parsed = JSON.parse(itemOrderInput.value || '{}');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (error) {
return {};
}
};
let hasPendingOrderChanges = false;
const markPendingOrderChanges = () => {
hasPendingOrderChanges = true;
if (orderStateIndicator) {
orderStateIndicator.hidden = false;
}
};
const syncCategoryButtonState = () => {
const sections = Array.from(equipmentSections.querySelectorAll('.equipment-section[data-category-key]'));
const firstKey = sections[0]?.dataset.categoryKey || '';
const lastKey = sections[sections.length - 1]?.dataset.categoryKey || '';
categoryButtons.forEach((button) => {
const categoryKey = button.dataset.categoryKey || '';
const direction = button.dataset.moveDirection || '';
button.disabled = (direction === 'up' && categoryKey === firstKey)
|| (direction === 'down' && categoryKey === lastKey);
});
};
const syncCategoryOrderState = () => {
const orderedKeys = Array.from(equipmentSections.querySelectorAll('.equipment-section[data-category-key]'))
.map((section) => section.dataset.categoryKey || '')
.filter(Boolean);
categoryOrderInput.value = orderedKeys.join(',');
syncCategoryButtonState();
};
const syncItemOrderStateForGrid = (grid) => {
const categoryKey = grid.dataset.categoryKey || '';
if (!categoryKey) {
return;
}
const state = readItemOrderState();
state[categoryKey] = Array.from(grid.querySelectorAll('.equip-card[data-item-id]'))
.map((card) => Number(card.dataset.itemId || 0))
.filter((itemId) => Number.isInteger(itemId) && itemId > 0);
itemOrderInput.value = JSON.stringify(state);
};
categoryButtons.forEach((button) => {
button.addEventListener('click', () => {
const categoryKey = button.dataset.categoryKey || '';
const direction = button.dataset.moveDirection || '';
const section = equipmentSections.querySelector(`.equipment-section[data-category-key="${categoryKey}"]`);
if (!section) {
return;
}
if (direction === 'up') {
const previousSection = section.previousElementSibling;
if (previousSection) {
equipmentSections.insertBefore(section, previousSection);
}
} else if (direction === 'down') {
const nextSection = section.nextElementSibling;
if (nextSection) {
equipmentSections.insertBefore(nextSection, section);
}
}
syncCategoryOrderState();
markPendingOrderChanges();
});
});
grids.forEach((grid) => {
const cards = Array.from(grid.querySelectorAll('.equip-card[data-item-id]'));
if (cards.length < 2) {
syncItemOrderStateForGrid(grid);
return;
}
let draggedCard = null;
let originalOrder = cards.map((card) => card.dataset.itemId).join(',');
const currentOrder = () => Array.from(grid.querySelectorAll('.equip-card[data-item-id]'))
.map((card) => card.dataset.itemId)
.filter(Boolean)
.join(',');
const clearDragMarkers = () => {
grid.querySelectorAll('.equip-card.drag-over').forEach((card) => card.classList.remove('drag-over'));
};
cards.forEach((card) => {
card.addEventListener('dragstart', (event) => {
draggedCard = card;
originalOrder = currentOrder();
card.classList.add('dragging');
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', card.dataset.itemId || '');
}
});
card.addEventListener('dragover', (event) => {
if (!draggedCard || draggedCard === card || draggedCard.parentElement !== grid) {
return;
}
event.preventDefault();
clearDragMarkers();
card.classList.add('drag-over');
const rect = card.getBoundingClientRect();
const insertBefore = event.clientY < rect.top + (rect.height / 2);
grid.insertBefore(draggedCard, insertBefore ? card : card.nextSibling);
});
card.addEventListener('dragleave', () => {
card.classList.remove('drag-over');
});
card.addEventListener('drop', (event) => {
event.preventDefault();
card.classList.remove('drag-over');
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
clearDragMarkers();
if (!draggedCard) {
return;
}
const updatedOrder = currentOrder();
draggedCard = null;
if (updatedOrder !== '' && updatedOrder !== originalOrder) {
syncItemOrderStateForGrid(grid);
markPendingOrderChanges();
}
});
});
grid.addEventListener('dragover', (event) => {
if (!draggedCard || draggedCard.parentElement !== grid) {
return;
}
event.preventDefault();
const targetCard = event.target.closest('.equip-card[data-item-id]');
if (!targetCard) {
grid.appendChild(draggedCard);
}
});
grid.addEventListener('drop', (event) => {
event.preventDefault();
clearDragMarkers();
});
syncItemOrderStateForGrid(grid);
});
editForm.addEventListener('submit', () => {
syncCategoryOrderState();
grids.forEach((grid) => syncItemOrderStateForGrid(grid));
hasPendingOrderChanges = false;
if (orderStateIndicator) {
orderStateIndicator.hidden = true;
}
});
syncCategoryOrderState();
}
initEquipmentOrdering();
</script>
</body>
</html>