1<?php
2/**
3 * Title: Helpdesk
4 * Icon: fa-life-ring
5 * Group: Support
6 * Order: 20
7 * Description: Answer, close and delete player support tickets.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15$view = intv($_GET['view'] ?? 0);
16
17// ---------------------------------------------------------------------------
18// Ticket actions
19// ---------------------------------------------------------------------------
20if ($_SERVER['REQUEST_METHOD'] === 'POST' && $view > 0) {
21
22 // ----------------------------------------------------------- Post reply
23 if (!empty($_POST['reply_text'])) {
24 $replyText = sanitize((string)$_POST['reply_text']);
25 $username = sanitize((string)($_POST['username'] ?? 'ADMIN'));
26
27 db()->execute("
28 INSERT INTO `znote_tickets_replies` (`tid`, `username`, `message`, `created`)
29 VALUES (?, ?, ?, ?);
30 ", [$view, $username, $replyText, time()]);
31
32 db()->execute("
33 UPDATE `znote_tickets`
34 SET `status` = 'Staff-Reply'
35 WHERE `id` = ?
36 LIMIT 1;
37 ", [$view]);
38
39 acp_log('helpdesk.reply', '#' . $view);
40 acp_flash_success(t('acp.hd.reply_posted'));
41 acp_redirect('helpdesk', ['view' => $view]);
42 }
43
44 $ticketId = intv($_POST['admin_ticket_id'] ?? 0);
45
46 // ---------------------------------------------------------- Close ticket
47 if (!empty($_POST['admin_ticket_close']) && $ticketId > 0) {
48 db()->execute("UPDATE `znote_tickets` SET `status` = 'CLOSED' WHERE `id` = ? LIMIT 1;", [$ticketId]);
49 acp_log('helpdesk.close', '#' . $ticketId);
50 acp_flash_success(t('acp.hd.closed'));
51 acp_redirect('helpdesk', ['view' => $view]);
52 }
53
54 // ----------------------------------------------------------- Open ticket
55 if (!empty($_POST['admin_ticket_open']) && $ticketId > 0) {
56 db()->execute("UPDATE `znote_tickets` SET `status` = 'Open' WHERE `id` = ? LIMIT 1;", [$ticketId]);
57 acp_log('helpdesk.reopen', '#' . $ticketId);
58 acp_flash_success(t('acp.hd.reopened'));
59 acp_redirect('helpdesk', ['view' => $view]);
60 }
61
62 // --------------------------------------------------------- Delete ticket
63 if (!empty($_POST['admin_ticket_delete']) && $ticketId > 0) {
64 db()->execute("DELETE FROM `znote_tickets` WHERE `id` = ? LIMIT 1;", [$ticketId]);
65 db()->execute("DELETE FROM `znote_tickets_replies` WHERE `tid` = ?;", [$ticketId]);
66 acp_log('helpdesk.delete', '#' . $ticketId);
67 acp_flash_success(t('acp.hd.deleted', ['id' => $ticketId]));
68 acp_redirect('helpdesk');
69 }
70}
71
72// Pill colour per ticket status.
73function acp_ticket_tone(string $status): string {
74 switch (strtoupper($status)) {
75 case 'CLOSED': return 'grey';
76 case 'STAFF-REPLY': return 'blue';
77 case 'OPEN': return 'green';
78 default: return 'amber';
79 }
80}
81?>
82
83<?php if ($view > 0):
84
85 $ticket = db()->fetchOne("SELECT * FROM `znote_tickets` WHERE `id` = ? LIMIT 1;", [$view]);
86
87 if (!is_array($ticket)):
88 ?>
89 <section class="acp-card">
90 <div class="acp-card-body">
91 <?php acp_empty(t('acp.hd.not_found'), 'fa-question-circle-o'); ?>
92 <div class="acp-actions" style="justify-content:center;">
93 <a class="acp-btn acp-btn--ghost" href="<?= h(acp_url('helpdesk')) ?>"><?= t('acp.hd.back_all') ?></a>
94 </div>
95 </div>
96 </section>
97 <?php
98 else:
99 $ticketId = (int)($ticket['id'] ?? $view);
100 $status = (string)($ticket['status'] ?? '');
101 $isClosed = (strtoupper($status) === 'CLOSED');
102
103 $replies = db()->fetchAll("
104 SELECT * FROM `znote_tickets_replies`
105 WHERE `tid` = ?
106 ORDER BY `created` ASC;
107 ", [$view]);
108 ?>
109
110 <div class="acp-toolbar">
111 <div>
112 <strong><?= t('acp.hd.ticket_hash', ['id' => $ticketId]) ?></strong>
113 — <?= h((string)($ticket['subject'] ?? '')) ?>
114 <span class="acp-pill acp-pill--<?= h(acp_ticket_tone($status)) ?>"><?= h($status) ?></span>
115 </div>
116 <div class="acp-actions is-tight">
117 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('helpdesk')) ?>">
118 <i class="fa fa-arrow-left"></i> <?= t('acp.hd.all_tickets') ?>
119 </a>
120 <form class="acp-inline-form" method="post">
121 <?= acp_csrf_field() ?>
122 <input type="hidden" name="admin_ticket_id" value="<?= $ticketId ?>">
123 <?php if (!$isClosed): ?>
124 <button class="acp-btn acp-btn--amber acp-btn--sm" type="submit" name="admin_ticket_close" value="1">
125 <i class="fa fa-lock"></i> <?= t('acp.hd.close') ?>
126 </button>
127 <?php else: ?>
128 <button class="acp-btn acp-btn--green acp-btn--sm" type="submit" name="admin_ticket_open" value="1">
129 <i class="fa fa-unlock"></i> <?= t('acp.hd.reopen') ?>
130 </button>
131 <?php endif; ?>
132 </form>
133 <form class="acp-inline-form" method="post" data-confirm="<?= h(t('acp.hd.confirm_delete')) ?>">
134 <?= acp_csrf_field() ?>
135 <input type="hidden" name="admin_ticket_id" value="<?= $ticketId ?>">
136 <button class="acp-btn acp-btn--red acp-btn--sm" type="submit" name="admin_ticket_delete" value="1">
137 <i class="fa fa-trash"></i> <?= t('acp.hd.delete') ?>
138 </button>
139 </form>
140 </div>
141 </div>
142
143 <article class="acp-post">
144 <header class="acp-post-head">
145 <span><?= t('acp.hd.opened_by') ?> <strong><?= h((string)($ticket['username'] ?? '')) ?></strong></span>
146 <span><?= h(getClock((int)($ticket['creation'] ?? 0), true)) ?></span>
147 </header>
148 <div class="acp-post-body"><?= nl2br(h((string)($ticket['message'] ?? ''))) ?></div>
149 </article>
150
151 <?php if (is_array($replies) && $replies): ?>
152 <?php foreach ($replies as $reply):
153 $replyUser = (string)($reply['username'] ?? '');
154 $isStaff = (strtoupper($replyUser) === 'ADMIN');
155 ?>
156 <article class="acp-post<?= $isStaff ? ' acp-post--staff' : '' ?>">
157 <header class="acp-post-head">
158 <span><?= $isStaff ? '<i class="fa fa-shield"></i> ' : '' ?><?= t('acp.hd.reply_by') ?> <strong><?= h($replyUser) ?></strong></span>
159 <span><?= h(getClock((int)($reply['created'] ?? 0), true)) ?></span>
160 </header>
161 <div class="acp-post-body"><?= nl2br(h((string)($reply['message'] ?? ''))) ?></div>
162 </article>
163 <?php endforeach; ?>
164 <?php endif; ?>
165
166 <?php if (!$isClosed): ?>
167 <section class="acp-card">
168 <header class="acp-card-head"><h2><?= t('acp.hd.reply_heading') ?></h2></header>
169 <div class="acp-card-body">
170 <form method="post">
171 <?= acp_csrf_field() ?>
172 <input type="hidden" name="username" value="ADMIN">
173 <div class="acp-field">
174 <textarea class="acp-textarea" name="reply_text" rows="7" placeholder="<?= h(t('acp.hd.reply_placeholder')) ?>" required></textarea>
175 </div>
176 <div class="acp-actions">
177 <button class="acp-btn" type="submit"><i class="fa fa-reply"></i> <?= t('acp.hd.post_reply') ?></button>
178 </div>
179 </form>
180 </div>
181 </section>
182 <?php else: ?>
183 <section class="acp-card">
184 <div class="acp-card-body">
185 <?php acp_empty(t('acp.hd.closed_hint'), 'fa-lock'); ?>
186 </div>
187 </section>
188 <?php endif; ?>
189
190 <?php endif; ?>
191
192<?php else:
193
194 $tickets = db()->fetchAll("
195 SELECT `id`, `subject`, `username`, `creation`, `status`
196 FROM `znote_tickets`
197 ORDER BY `creation` DESC;
198 ");
199 ?>
200
201 <section class="acp-card">
202 <header class="acp-card-head">
203 <h2><?= t('acp.hd.all_tickets') ?></h2>
204 <p><?= t('acp.hd.newest_first') ?></p>
205 </header>
206 <div class="acp-card-body is-flush">
207 <?php if (is_array($tickets) && $tickets): ?>
208 <div class="acp-table-wrap">
209 <table class="acp-table">
210 <thead>
211 <tr>
212 <th>#</th>
213 <th><?= t('acp.hd.col_subject') ?></th>
214 <th><?= t('acp.hd.col_opened_by') ?></th>
215 <th><?= t('acp.hd.col_created') ?></th>
216 <th><?= t('acp.hd.col_status') ?></th>
217 </tr>
218 </thead>
219 <tbody>
220 <?php foreach ($tickets as $ticket):
221 $id = (int)($ticket['id'] ?? 0);
222 $status = (string)($ticket['status'] ?? '');
223 ?>
224 <tr>
225 <td class="is-muted"><?= $id ?></td>
226 <td><a href="<?= h(acp_url('helpdesk', ['view' => $id])) ?>"><?= h((string)($ticket['subject'] ?? '')) ?></a></td>
227 <td><?= h((string)($ticket['username'] ?? '')) ?></td>
228 <td class="is-nowrap is-muted"><?= h(getClock((int)($ticket['creation'] ?? 0), true)) ?></td>
229 <td><span class="acp-pill acp-pill--<?= h(acp_ticket_tone($status)) ?>"><?= h($status) ?></span></td>
230 </tr>
231 <?php endforeach; ?>
232 </tbody>
233 </table>
234 </div>
235 <?php else: ?>
236 <?php acp_empty(t('acp.hd.empty'), 'fa-life-ring'); ?>
237 <?php endif; ?>
238 </div>
239 </section>
240
241<?php endif; ?>
242