execute(" CREATE TABLE IF NOT EXISTS `znote_shop_offers` ( `id` int NOT NULL AUTO_INCREMENT, `type` int NOT NULL, `itemid` int DEFAULT NULL, `count` int NOT NULL DEFAULT '1', `description` varchar(255) NOT NULL, `points` int NOT NULL DEFAULT '10', `active` tinyint NOT NULL DEFAULT '1', `sort_order` int NOT NULL DEFAULT '0', `created_by` int DEFAULT NULL, `created_at` int DEFAULT NULL, `updated_at` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `active_sort` (`active`, `sort_order`, `id`) ) ENGINE=InnoDB; "); function acp_shop_offer_columns(): array { $columns = []; $rows = db()->fetchAll("SHOW COLUMNS FROM `znote_shop_offers`;"); if (is_array($rows)) { foreach ($rows as $row) { if (!empty($row['Field'])) { $columns[(string)$row['Field']] = true; } } } return $columns; } function acp_shop_ensure_offer_schema(): void { $columns = acp_shop_offer_columns(); if (empty($columns['active'])) { db()->execute("ALTER TABLE `znote_shop_offers` ADD `active` tinyint NOT NULL DEFAULT '1' AFTER `points`;"); $columns['active'] = true; } if (empty($columns['sort_order'])) { db()->execute("ALTER TABLE `znote_shop_offers` ADD `sort_order` int NOT NULL DEFAULT '0' AFTER `active`;"); $columns['sort_order'] = true; } if (empty($columns['updated_at'])) { db()->execute("ALTER TABLE `znote_shop_offers` ADD `updated_at` int DEFAULT NULL AFTER `created_at`;"); $columns['updated_at'] = true; } $indexes = db()->fetchAll("SHOW INDEX FROM `znote_shop_offers` WHERE `Key_name` = 'active_sort';"); if (empty($indexes) && !empty($columns['active']) && !empty($columns['sort_order'])) { db()->execute("ALTER TABLE `znote_shop_offers` ADD KEY `active_sort` (`active`, `sort_order`, `id`);"); } } acp_shop_ensure_offer_schema(); function acp_shop_pack_outfit_pair(string $raw): int|false { if (!preg_match('/^\s*(\d+)\s*,\s*(\d+)\s*$/', $raw, $m)) { return false; } return ((int)$m[1] * 10000) + (int)$m[2]; } function acp_shop_unpack_outfit_pair(int $packed): string { $male = (int)floor($packed / 10000); $female = (int)($packed % 10000); return $male . ',' . $female; } function acp_shop_offer_type_label(int $type): string { $types = [ 1 => t('acp.shp.type_item'), 2 => t('acp.shp.type_premium'), 3 => t('acp.shp.type_gender'), 4 => t('acp.shp.type_name'), 5 => t('acp.shp.type_outfit'), 6 => t('acp.shp.type_mount'), 7 => t('acp.shp.type_custom'), 8 => t('acp.shp.type_custom'), ]; return $types[$type] ?? t('acp.shp.type_custom'); } function acp_shop_public_asset_url(string $url): string { if ($url === '' || preg_match('~^(https?:)?//~i', $url) || $url[0] === '/') { return $url; } return acp_site($url); } function acp_shop_item_image_url(int $itemId): string { global $config; if (function_exists('znote_item_image_url')) { return acp_shop_public_asset_url(znote_item_image_url($itemId)); } $server = trim((string)($config['shop']['imageServer'] ?? ''), '/'); $typeExt = trim((string)($config['shop']['imageType'] ?? 'gif'), '.'); if ($server === '' || $itemId <= 0) { return ''; } if (preg_match('~^https?://~i', $server)) { return $server . '/' . $itemId . '.' . $typeExt; } if (str_contains($server, '.')) { return 'http://' . $server . '/' . $itemId . '.' . $typeExt; } return acp_shop_public_asset_url($server . '/' . $itemId . '.' . $typeExt); } function acp_shop_outfit_server_url(): string { global $config; $server = trim((string)($config['show_outfits']['imageServer'] ?? '')); if ($server === '') { return ''; } if (preg_match('~^(https?:)?//~i', $server) || substr($server, 0, 1) === '/') { return $server; } return acp_site($server); } function acp_shop_outfit_image_url(int $outfitId, int $addons, int $mountId = 0): string { $server = acp_shop_outfit_server_url(); if ($server === '' || $outfitId <= 0) { return ''; } $url = $server . '?id=' . $outfitId . '&addons=' . $addons . '&head=78&body=68&legs=58&feet=76&direction=2'; if ($mountId > 0) { $url .= '&mount=' . $mountId; } return $url; } function acp_shop_offer_preview(array $offer, array $items): string { global $config; $type = intv($offer['type'] ?? 0); $itemId = intv($offer['itemid'] ?? 0); $count = intv($offer['count'] ?? 0); if ($type === 5 && !empty($config['show_outfits']['imageServer'])) { $male = (int)floor($itemId / 10000); $female = (int)($itemId % 10000); return '' . ''; } if ($type === 6 && !empty($config['show_outfits']['imageServer'])) { return ''; } if (!empty($config['shop']['showImage']) && $itemId > 0) { return ''; } return '' . h($items[$itemId] ?? '#' . $itemId) . ''; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $shopAction = (string)($_POST['shop_offer_action'] ?? ''); $now = time(); if ($shopAction === 'add') { $type = intv($_POST['type'] ?? 0); $itemRaw = trim((string)($_POST['itemid'] ?? '')); $count = max(0, intv($_POST['count'] ?? 0)); $points = max(1, intv($_POST['points'] ?? 0)); $sortOrder = intv($_POST['sort_order'] ?? 0); $description = substr(trim((string)($_POST['description'] ?? '')), 0, 255); $itemId = false; if ($type === 5) { $itemId = acp_shop_pack_outfit_pair($itemRaw); if ($itemId === false) { acp_flash_error(t('acp.shp.outfit_field_error')); } } elseif (preg_match('/^\d+$/', $itemRaw)) { $itemId = (int)$itemRaw; } if ($type <= 0 || $description === '' || $points <= 0 || $itemId === false) { acp_flash_error(t('acp.shp.missing_fields')); } else { $created = db()->execute(" INSERT INTO `znote_shop_offers` (`type`, `itemid`, `count`, `description`, `points`, `active`, `sort_order`, `created_by`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?); ", [$type, $itemId, $count, $description, $points, $sortOrder, (int)$user_data['id'], $now, $now]); if ($created) { acp_log('shop.offer_add', $description, ['type' => $type, 'points' => $points]); acp_flash_success(t('acp.shp.offer_added')); } else { acp_flash_error(t('acp.shp.offer_add_failed')); } acp_redirect('shop'); } } if ($shopAction === 'toggle') { $id = intv($_POST['id'] ?? 0); $updated = db()->execute(" UPDATE `znote_shop_offers` SET `active` = CASE WHEN `active` = 1 THEN 0 ELSE 1 END, `updated_at` = ? WHERE `id` = ? LIMIT 1; ", [$now, $id]); if ($updated) { acp_log('shop.offer_toggle', '#' . $id); } $updated ? acp_flash_success(t('acp.shp.status_updated')) : acp_flash_error(t('acp.shp.status_failed')); acp_redirect('shop'); } if ($shopAction === 'delete') { $id = intv($_POST['id'] ?? 0); $deleted = db()->execute("DELETE FROM `znote_shop_offers` WHERE `id` = ? LIMIT 1;", [$id]); if ($deleted) { acp_log('shop.offer_delete', '#' . $id); } $deleted ? acp_flash_success(t('acp.shp.removed')) : acp_flash_error(t('acp.shp.remove_failed')); acp_redirect('shop'); } if (in_array($shopAction, ['bulk_enable', 'bulk_disable', 'bulk_delete'], true)) { $ids = array_values(array_filter(array_map('intv', (array)($_POST['ids'] ?? [])))); if (!$ids) { acp_flash_error(t('acp.shp.bulk_none_selected')); acp_redirect('shop'); } $placeholders = implode(',', array_fill(0, count($ids), '?')); if ($shopAction === 'bulk_delete') { $done = db()->execute("DELETE FROM `znote_shop_offers` WHERE `id` IN ({$placeholders});", $ids); } else { $active = ($shopAction === 'bulk_enable') ? 1 : 0; $done = db()->execute(" UPDATE `znote_shop_offers` SET `active` = ?, `updated_at` = ? WHERE `id` IN ({$placeholders}); ", array_merge([$active, $now], $ids)); } if ($done !== false) { acp_log('shop.offer_bulk_' . $shopAction, implode(',', $ids), ['count' => count($ids)]); acp_flash_success(t('acp.shp.bulk_done', ['n' => count($ids)])); } else { acp_flash_error(t('acp.shp.bulk_failed')); } acp_redirect('shop'); } } $offers = db()->fetchAll("SELECT * FROM `znote_shop_offers` ORDER BY `active` DESC, `sort_order` ASC, `id` ASC;"); $offers = is_array($offers) ? $offers : []; $activeOffers = 0; $hiddenOffers = 0; foreach ($offers as $offer) { !empty($offer['active']) ? $activeOffers++ : $hiddenOffers++; } $itemImageTemplate = acp_shop_item_image_url(999999); if ($itemImageTemplate !== '') { $itemImageTemplate = str_replace('999999', '{id}', $itemImageTemplate); } ?>

8): ?>
#

- ,