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);
}
?>
= h(t('acp.shp.add_offer_sub')) ?>
= h(t('acp.shp.db_offers_sub')) ?>
| # | = h(t('acp.shp.col_preview')) ?> | = h(t('acp.shp.col_offer')) ?> | = h(t('acp.shp.col_points')) ?> | = h(t('acp.shp.col_status')) ?> | = h(t('acp.shp.col_actions')) ?> | |
|---|---|---|---|---|---|---|
| = intv($offer['id'] ?? 0) ?> | = acp_shop_offer_preview($offer, $items) ?> |
= h($offer['description'] ?? '') ?> = h(acp_shop_offer_type_label(intv($offer['type'] ?? 0))) ?> - = intv($offer['type'] ?? 0) === 5 ? h(acp_shop_unpack_outfit_pair(intv($offer['itemid'] ?? 0))) : intv($offer['itemid'] ?? 0) ?>, = h(t('acp.shp.count_label2')) ?> = intv($offer['count'] ?? 0) ?> |
= intv($offer['points'] ?? 0) ?> | = !empty($offer['active']) ? h(t('acp.shp.active_pill')) : h(t('acp.shp.hidden_pill')) ?> |