1<?php
2/**
3 * Title: Character Auctions
4 * Icon: fa-gavel
5 * Group: Economy
6 * Order: 20
7 * Description: Ongoing, unclaimed and completed character sales.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15$auction = $config['shop_auction'] ?? [];
16$storageAccountId = (int)($auction['storage_account_id'] ?? 0);
17$now = time();
18
19function acp_duration(int $seconds): string {
20 if ($seconds <= 0) {
21 return t('acp.auc.zero_seconds');
22 }
23
24 $units = [
25 'day' => [86400, 'acp.auc.day', 'acp.auc.days'],
26 'hour' => [3600, 'acp.auc.hour', 'acp.auc.hours'],
27 'minute' => [60, 'acp.auc.minute', 'acp.auc.minutes'],
28 'second' => [1, 'acp.auc.second', 'acp.auc.seconds'],
29 ];
30
31 $parts = [];
32 foreach ($units as [$value, $oneKey, $manyKey]) {
33 $qty = intdiv($seconds, $value);
34 if ($qty > 0) {
35 $seconds -= $qty * $value;
36 $parts[] = $qty . ' ' . t($qty === 1 ? $oneKey : $manyKey);
37 }
38 }
39
40 return implode(', ', $parts);
41}
42
43// ---------------------------------------------------------------------------
44// Passive sweep: a bid period that ran out with a bidder on it is a sale.
45// ---------------------------------------------------------------------------
46$expired = db()->fetchAll("
47 SELECT `id`
48 FROM `znote_auction_player`
49 WHERE `sold` = 0
50 AND `time_end` < ?
51 AND `bidder_account_id` > 0;
52", [$now]);
53
54if (is_array($expired) && $expired) {
55 $soldIds = array_map(static fn($a) => (int)$a['id'], $expired);
56 $placeholders = implode(',', array_fill(0, count($soldIds), '?'));
57 db()->execute("
58 UPDATE `znote_auction_player`
59 SET `sold` = 1
60 WHERE `id` IN ({$placeholders});
61 ", $soldIds);
62}
63
64// ---------------------------------------------------------------------------
65// The three lists
66// ---------------------------------------------------------------------------
67$characterFields = "
68 `za`.`id` AS `zaid`,
69 `za`.`price`,
70 `za`.`bid`,
71 `za`.`time_begin`,
72 `za`.`time_end`,
73 `p`.`id` AS `player_id`,
74 `p`.`name`,
75 `p`.`vocation`,
76 `p`.`level`
77";
78
79$pending = db()->fetchAll("
80 SELECT {$characterFields}
81 FROM `znote_auction_player` `za`
82 INNER JOIN `players` `p` ON `za`.`player_id` = `p`.`id`
83 WHERE `p`.`account_id` = ?
84 AND `za`.`claimed` = 0
85 AND `za`.`sold` = 1
86 ORDER BY `za`.`time_end` DESC;
87", [$storageAccountId]);
88
89$ongoing = db()->fetchAll("
90 SELECT {$characterFields}
91 FROM `znote_auction_player` `za`
92 INNER JOIN `players` `p` ON `za`.`player_id` = `p`.`id`
93 WHERE `p`.`account_id` = ?
94 AND `za`.`sold` = 0
95 ORDER BY `za`.`time_end` DESC;
96", [$storageAccountId]);
97
98$completed = db()->fetchAll("
99 SELECT {$characterFields}
100 FROM `znote_auction_player` `za`
101 INNER JOIN `players` `p` ON `za`.`player_id` = `p`.`id`
102 WHERE `za`.`claimed` = 1
103 ORDER BY `za`.`time_end` DESC;
104");
105
106$pending = is_array($pending) ? $pending : [];
107$ongoing = is_array($ongoing) ? $ongoing : [];
108$completed = is_array($completed) ? $completed : [];
109?>
110
111<div class="acp-stats">
112 <?php
113 acp_stat(t('acp.auc.stat_ongoing'), count($ongoing), 'fa-gavel', null, 'blue');
114 acp_stat(t('acp.auc.stat_pending'), count($pending), 'fa-hourglass-half', null, 'amber');
115 acp_stat(t('acp.auc.stat_completed'), count($completed), 'fa-check-circle', null, 'green');
116 ?>
117</div>
118
119<?php if ($storageAccountId <= 0): ?>
120 <div class="acp-flash acp-flash--error">
121 <i class="fa fa-exclamation-triangle"></i>
122 <span>
123 <?= t('acp.auc.no_storage', ['code' => '<code>$config[\'shop_auction\'][\'storage_account_id\']</code>']) ?>
124 </span>
125 </div>
126<?php endif; ?>
127
128<!-- ------------------------------------------------------------- Ongoing -->
129<section class="acp-card">
130 <header class="acp-card-head">
131 <h2><?= t('acp.auc.ongoing_title') ?></h2>
132 <p><?= t('acp.auc.ongoing_sub') ?></p>
133 </header>
134 <div class="acp-card-body is-flush">
135 <?php if ($ongoing): ?>
136 <div class="acp-table-wrap">
137 <table class="acp-table">
138 <thead>
139 <tr>
140 <th class="is-num"><?= t('acp.auc.col_level') ?></th>
141 <th><?= t('acp.auc.col_vocation') ?></th>
142 <th class="is-num"><?= t('acp.auc.col_price') ?></th>
143 <th class="is-num"><?= t('acp.auc.col_bid') ?></th>
144 <th><?= t('acp.auc.col_listed') ?></th>
145 <th><?= t('acp.auc.col_type') ?></th>
146 <th class="is-num"> </th>
147 </tr>
148 </thead>
149 <tbody>
150 <?php foreach ($ongoing as $c):
151 $ended = $now > (int)$c['time_end'];
152 ?>
153 <tr>
154 <td class="is-num"><?= (int)$c['level'] ?></td>
155 <td><?= h(vocation_id_to_name((int)$c['vocation'])) ?></td>
156 <td class="is-num"><?= (int)$c['price'] ?></td>
157 <td class="is-num"><?= (int)$c['bid'] ?></td>
158 <td class="is-nowrap is-muted"><?= h(getClock((int)$c['time_begin'], true)) ?></td>
159 <td>
160 <?php if ($ended): ?>
161 <span class="acp-pill acp-pill--blue"><?= t('acp.auc.instant_buy') ?></span>
162 <?php else: ?>
163 <span class="acp-pill acp-pill--amber"><?= t('acp.auc.bidding') ?></span>
164 <span class="is-muted"><?= t('acp.auc.left', ['time' => acp_duration((int)$c['time_end'] - $now)]) ?></span>
165 <?php endif; ?>
166 </td>
167 <td class="is-num is-nowrap">
168 <a class="acp-btn acp-btn--ghost acp-btn--sm"
169 href="<?= h(acp_site('auctionChar.php?action=view&zaid=' . (int)$c['zaid'])) ?>"
170 target="_blank" rel="noopener">
171 <i class="fa fa-eye"></i> <?= t('acp.auc.view') ?>
172 </a>
173 </td>
174 </tr>
175 <?php endforeach; ?>
176 </tbody>
177 </table>
178 </div>
179 <?php else: ?>
180 <?php acp_empty(t('acp.auc.none_ongoing'), 'fa-gavel'); ?>
181 <?php endif; ?>
182 </div>
183</section>
184
185<?php
186/**
187 * Sold/completed lists share the same shape.
188 *
189 * @param array<int, array<string, mixed>> $rows
190 */
191function acp_auction_table(array $rows, string $title, string $subtitle, string $emptyText): void {
192 ?>
193 <section class="acp-card">
194 <header class="acp-card-head">
195 <h2><?= h($title) ?></h2>
196 <p><?= h($subtitle) ?></p>
197 </header>
198 <div class="acp-card-body is-flush">
199 <?php if (!$rows): ?>
200 <?php acp_empty($emptyText, 'fa-archive'); ?>
201 <?php else: ?>
202 <div class="acp-table-wrap">
203 <table class="acp-table">
204 <thead>
205 <tr>
206 <th><?= t('acp.auc.col_character') ?></th>
207 <th class="is-num"><?= t('acp.auc.col_level') ?></th>
208 <th><?= t('acp.auc.col_vocation') ?></th>
209 <th class="is-num"><?= t('acp.auc.col_price') ?></th>
210 <th class="is-num"><?= t('acp.auc.col_bid') ?></th>
211 <th><?= t('acp.auc.col_listed') ?></th>
212 <th><?= t('acp.auc.col_ended') ?></th>
213 </tr>
214 </thead>
215 <tbody>
216 <?php foreach ($rows as $c): ?>
217 <tr>
218 <td class="is-nowrap">
219 <a href="<?= h(acp_site('characterprofile.php?name=' . urlencode((string)$c['name']))) ?>" target="_blank" rel="noopener">
220 <?= h((string)$c['name']) ?>
221 </a>
222 </td>
223 <td class="is-num"><?= (int)$c['level'] ?></td>
224 <td><?= h(vocation_id_to_name((int)$c['vocation'])) ?></td>
225 <td class="is-num"><?= (int)$c['price'] ?></td>
226 <td class="is-num"><strong><?= (int)$c['bid'] ?></strong></td>
227 <td class="is-nowrap is-muted"><?= h(getClock((int)$c['time_begin'], true)) ?></td>
228 <td class="is-nowrap is-muted"><?= h(getClock((int)$c['time_end'], true)) ?></td>
229 </tr>
230 <?php endforeach; ?>
231 </tbody>
232 </table>
233 </div>
234 <?php endif; ?>
235 </div>
236 </section>
237 <?php
238}
239
240acp_auction_table(
241 $pending,
242 t('acp.auc.pending_title'),
243 t('acp.auc.pending_sub'),
244 t('acp.auc.pending_empty')
245);
246
247acp_auction_table(
248 $completed,
249 t('acp.auc.completed_title'),
250 t('acp.auc.completed_sub'),
251 t('acp.auc.completed_empty')
252);
253?>
254
255<section class="acp-card">
256 <details>
257 <summary class="acp-card-head" style="cursor:pointer;">
258 <h2><?= t('acp.auc.how_title') ?></h2>
259 <p><?= t('acp.auc.how_sub', ['code' => '<code>$config[\'shop_auction\']</code>']) ?></p>
260 </summary>
261 <div class="acp-card-body">
262 <p>
263 <?= t('acp.auc.how_text') ?>
264 </p>
265 <?php data_dump($auction, false, "config.php: shop_auction"); ?>
266 </div>
267 </details>
268</section>
269