14 private function db() {
15 return dbx()->get_system_obj(
'dbxDB');
18 private function tpl() {
19 return dbx()->get_system_obj(
'dbxTPL');
22 private function h(
$value):
string {
23 return htmlspecialchars((
string)
$value, ENT_QUOTES,
'UTF-8');
26 private function alert(
string $type,
string $message):
string {
27 return $this->tpl()->get_tpl(
'dbx|alert-' . $type, array(
'msg' => $message));
30 private function schemaReady():
bool {
32 $server =
'dbxContact|dbxContact.db3';
37 $tables =
$db->select_query(
$server,
"SELECT name FROM sqlite_master WHERE type='table' AND name='contact_message' LIMIT 1");
38 if (!is_array($tables) || empty($tables)) {
42 $columns =
$db->select_query(
$server,
'PRAGMA table_info(contact_request)');
44 foreach ((array) $columns as $column) {
45 $names[] = (string) ($column[
'name'] ??
'');
48 return in_array(
'priority', $names,
true)
49 && in_array(
'last_activity_date', $names,
true)
50 && in_array(
'user_hidden', $names,
true);
53 private function statusClass(
string $status):
string {
55 'open' =>
'text-bg-primary',
56 'in_progress' =>
'text-bg-info',
57 'waiting_customer' =>
'text-bg-warning',
58 'answered' =>
'text-bg-success',
59 'closed' =>
'text-bg-secondary',
60 )[$status] ??
'text-bg-light';
63 private function priorityClass(
string $priority):
string {
65 'low' =>
'text-bg-secondary',
66 'normal' =>
'text-bg-primary',
67 'high' =>
'text-bg-warning',
68 'urgent' =>
'text-bg-danger',
69 )[$priority] ??
'text-bg-light';
72 private function counts(): array {
83 private function reportStatusStatsHtml(
$report):
string {
84 $counts = $this->counts();
86 .
'<span class="dbx-report-bar-stat ms-3"><span class="badge text-bg-primary">' . $this->h(
$report->get_fd_message(
'stat_open')) .
': ' . (int) $counts[
'count_open'] .
'</span></span>'
87 .
'<span class="dbx-report-bar-stat"><span class="badge text-bg-info">' . $this->h(
$report->get_fd_message(
'stat_progress')) .
': ' . (int) $counts[
'count_progress'] .
'</span></span>'
88 .
'<span class="dbx-report-bar-stat"><span class="badge text-bg-warning">' . $this->h(
$report->get_fd_message(
'stat_waiting')) .
': ' . (int) $counts[
'count_waiting'] .
'</span></span>'
89 .
'<span class="dbx-report-bar-stat"><span class="badge text-bg-success">' . $this->h(
$report->get_fd_message(
'stat_answered')) .
': ' . (int) $counts[
'count_answered'] .
'</span></span>'
90 .
'<span class="dbx-report-bar-stat"><span class="badge text-bg-secondary">' . $this->h(
$report->get_fd_message(
'stat_closed')) .
': ' . (int) $counts[
'count_closed'] .
'</span></span>';
93 private function frame(
string $content,
string $title =
''):
string {
94 $texts =
dbx()->get_system_obj(
'dbxForm');
95 $texts->init(
'contact-admin-texts',
'dbx|form');
96 $texts->load_fd_messages(
'dbxContact_admin|rpt-ticket-selection');
98 $title =
$texts->get_fd_message(
'frame_title');
101 .
'<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxContact_admin&dbx_run1=install"><i class="bi bi-database-gear"></i> '
102 . $this->h(
$texts->get_fd_message(
'install'))
104 .
'<a class="btn btn-primary btn-sm" href="?dbx_modul=dbxContact_admin&dbx_run1=list"><i class="bi bi-arrow-clockwise"></i> '
105 . $this->h(
$texts->get_fd_message(
'refresh'))
108 return $this->tpl()->get_tpl(
'dbxContact_admin|ticket-admin-shell', array_merge(
111 'bar_title' => $this->h($title),
112 'bar_icon' =>
'bi-life-preserver',
113 'bar_subtitle' => $this->h(
114 $texts->get_fd_message(
'frame_subtitle')
116 'bar_title_pre' =>
'',
117 'bar_title_heading_attrs' =>
'',
126 private function install():
string {
127 $dd =
dbx()->get_system_obj(
'dbxDD');
130 foreach (array(
'contactMessage') as $name) {
131 $dd->sync_dd_to_db(
'dbxContact', $name,
'reset');
133 for ($i = 0; $i < 100; $i++) {
134 $state =
$dd->sync_dd_to_db(
'dbxContact', $name,
'apply');
135 if (in_array((
string) (
$state[
'status'] ??
''), array(
'finished',
'error'),
true)) {
143 if ((
string) (
$state[
'status'] ??
'') !==
'finished') {
144 return $this->frame($this->alert(
'danger',
'Installation von ' . $this->h($name) .
' fehlgeschlagen: ' . $this->h(
$state[
'message'] ??
'unbekannt')));
148 $name =
'contactRequest';
149 $dd->sync_dd_to_db(
'dbxContact', $name,
'reset');
150 for ($i = 0; $i < 100; $i++) {
151 $state =
$dd->sync_dd_to_db(
'dbxContact', $name,
'force');
152 if (in_array((
string) (
$state[
'status'] ??
''), array(
'finished',
'error'),
true)) {
159 if ((
string) (
$state[
'status'] ??
'') !==
'finished') {
160 return $this->frame($this->alert(
'danger',
'Installation von ' . $this->h($name) .
' fehlgeschlagen: ' . $this->h(
$state[
'message'] ??
'unbekannt')));
166 foreach ((array)
$rows as $ticket) {
168 if (trim((
string) ($ticket[
'last_activity_date'] ??
'')) ===
'') {
169 $last = (string) ($ticket[
'create_date'] ?? date(
'Y-m-d H:i:s'));
172 'last_activity_date' => $last,
173 ), (
int) ($ticket[
'id'] ?? 0), 0, 1, 1, 0);
179 $this->alert(
'success',
'Ticket-Datenmodell installiert.')
180 . ($contentPageId > 0
181 ?
'<div class="alert alert-info mx-3">Die dbxContent-Seite „Meine Anfragen“ ist unter <a href="meine-anfragen">meine-anfragen</a> verfuegbar.</div>'
182 :
'<div class="alert alert-warning mx-3">Die dbxContent-Seite „Meine Anfragen“ konnte nicht automatisch angelegt werden.</div>')
183 .
'<div class="p-3"><a class="btn btn-primary" href="?dbx_modul=dbxContact_admin&dbx_run1=list"><i class="bi bi-list-ul"></i> Tickets anzeigen</a></div>'
187 private function whereForReport(
string $search,
string $status,
string $priority):
string {
193 $clauses[] =
"status = '" .
$db->escape($status,
$server) .
"'";
196 $clauses[] =
"priority = '" .
$db->escape($priority,
$server) .
"'";
198 if ($search !==
'') {
200 $clauses[] =
"(subject LIKE '%" . $needle .
"%' OR name LIKE '%" . $needle .
"%' OR email LIKE '%" . $needle .
"%' OR message LIKE '%" . $needle .
"%')";
203 return implode(
' AND ', $clauses);
206 private function decorateRows(array
$rows,
$report): array {
209 foreach (
$rows as $row) {
210 $rid = (int) ($row[
'id'] ?? 0);
213 $row[
'status_view'] =
'<span class="badge ' . $this->statusClass($status) .
'">' . $this->h(
$report->get_fd_message(
'status_' . $status, $status)) .
'</span>';
214 $row[
'priority_view'] =
'<span class="badge ' . $this->priorityClass($priority) .
'">' . $this->h(
$report->get_fd_message(
'priority_' . $priority, $priority)) .
'</span>';
215 $row[
'user_view'] = (int) ($row[
'uid'] ?? 0) > 0
216 ?
'#' . (int) $row[
'uid']
217 :
$report->get_fd_message(
'guest');
218 $row[
'hidden_view'] = (int) ($row[
'user_hidden'] ?? 0) === 1
219 ?
'<span class="badge text-bg-dark">'
220 . $this->h(
$report->get_fd_message(
'user_hidden'))
224 $row[
'action'] = $this->tpl()->get_tpl(
'dbxContact_admin|ticket-row-action', array(
'rid' => $rid));
230 private function deleteConfirmText(
int $rid,
int $messageCount,
$source): array {
231 $hasThread = $messageCount > 0;
234 'title' =>
$source->get_fd_message(
'delete_title'),
235 'question' =>
$source->format_fd_message(
241 'delete_hint_thread',
242 array(
'count' => $messageCount)
244 :
$source->get_fd_message(
'delete_hint_empty'),
249 if (!is_array($data) || (string) ($data[
'type'] ??
'') !==
'delete') {
253 $rid = (int) ($data[
'record'][
'id'] ?? $data[
'data'][
'rid'] ?? 0);
254 $messageCount = max(0, (
int) ($data[
'record'][
'message_count'] ?? 0));
256 $data[
'data'][
'action'] =
'?dbx_modul=dbxContact_admin&dbx_run1=delete&rid=' . $rid;
257 $data[
'data'][
'confirm_title'] =
$confirm[
'title'];
258 $data[
'data'][
'confirm'] =
$confirm[
'question'];
259 $data[
'data'][
'confirm_hint'] =
$confirm[
'hint'];
260 $data[
'data'][
'tooltip'] =
$report->get_fd_message(
267 private function listTickets(
string $success =
'',
string $error =
'', ?
bool $withFrame =
null): string {
268 if ($withFrame === null) {
269 $withFrame = (int)
dbx()->get_system_var(
'dbx_ajax', 0,
'int') !== 1;
274 $report->init(
'contact-ticket-report',
'dbxContact_admin|ticket-report');
275 $report->_fd =
'dbxContact_admin|rpt-ticket-selection';
278 $report->_action =
'?dbx_modul=dbxContact_admin&dbx_run1=list';
280 $report->_create_row_select =
false;
281 $report->_create_row_edit =
false;
282 $report->_create_row_delete =
true;
283 $report->_table_buttons =
'left';
290 'report_extra_stats',
291 $this->reportStatusStatsHtml(
$report)
293 if ($success !==
'') {
302 $report->set_callback_owner($this);
303 $report->set_callback(
'row_action_data',
'contactTicketReportRowActionData');
304 $report->set_tabel_tpl(
'tpl_row_delete',
'modul|ticket-row-delete');
305 $report->create_selection_fields(
'dbxContact_admin|rpt-ticket-selection');
308 $report->get_fd_message(
'report_title')
311 $search = trim((
string)
$report->get_fld_val(
'dbx_rwhere',
'',
'sqlsearch|max=100'));
312 $status = (string)
$report->get_fld_val(
'dbx_rstatus',
'all',
'parameter');
313 $priority = (string)
$report->get_fld_val(
'dbx_rpriority',
'all',
'parameter');
314 $rowsPerPage = max(10, min(100, (
int)
$report->get_fld_val(
'dbx_rrows', 30,
'int')));
315 $position = max(0, (
int)
$report->get_fld_val(
'dbx_rpos', 0,
'int'));
316 $sort = (string)
$report->get_fld_val(
'dbx_rsort',
'last_activity_date',
'parameter');
317 $direction = strtoupper((
string)
$report->get_fld_val(
'dbx_rdesc',
'DESC',
'parameter')) ===
'ASC' ?
'ASC' :
'DESC';
318 if (!in_array($sort, array(
'last_activity_date',
'create_date',
'id',
'status',
'priority',
'subject'),
true)) {
319 $sort =
'last_activity_date';
322 $where = $this->whereForReport($search, $status, $priority);
323 $cols = array(
'id',
'create_date',
'last_activity_date',
'status',
'priority',
'uid',
'name',
'email',
'subject',
'user_hidden');
325 'id' =>
$report->get_fd_message(
'column_ticket'),
326 'last_activity_date' =>
$report->get_fd_message(
329 'status_view' =>
$report->get_fd_message(
'column_status'),
330 'priority_view' =>
$report->get_fd_message(
'column_priority'),
331 'user_view' =>
$report->get_fd_message(
'column_user'),
332 'name' =>
$report->get_fd_message(
'column_name'),
333 'email' =>
$report->get_fd_message(
'column_email'),
334 'subject' =>
$report->get_fd_message(
'column_subject'),
336 'action' =>
$report->get_fd_message(
'column_action'),
339 'last_activity_date' =>
'php-datetime-usr',
340 'status_view' =>
'html',
341 'priority_view' =>
'html',
342 'hidden_view' =>
'html',
345 $report->_rrows = $rowsPerPage;
350 $report->_rdata = $this->decorateRows(
359 private function deleteTicketRecord(
int $rid): bool {
360 if ($rid <= 0 || !dbxContactTicket::ticket($this->db(), $rid)) {
367 if (
$db->begin(dbxContactTicket::DD_TICKET) === 1) {
368 $messagesOk =
$db->delete(dbxContactTicket::DD_MESSAGE, array(
'ticket_id' => $rid), 0, 1);
369 if ($messagesOk === 1 ||
$db->count(dbxContactTicket::DD_MESSAGE, array(
'ticket_id' => $rid)) === 0) {
370 $ok =
$db->delete(dbxContactTicket::DD_TICKET, $rid, 0, 1);
373 $db->commit(dbxContactTicket::DD_TICKET);
375 $db->rollback(dbxContactTicket::DD_TICKET);
382 private function timeline(array $ticket,
$texts): string {
384 dbxContactTicket::ensureInitialMessage(
$db, $ticket);
387 foreach (dbxContactTicket::messages(
$db, (
int) $ticket[
'id'],
true) as $message) {
388 $internal = (string) ($message[
'visibility'] ??
'public') ===
'internal';
389 $authorType = (string) ($message[
'author_type'] ??
'system');
390 $statusTo = trim((
string) ($message[
'status_to'] ??
''));
391 $html .= $this->tpl()->get_tpl(
'dbxContact_admin|ticket-message', array(
392 'message_class' => $internal ?
'border-warning bg-warning-subtle' : ($authorType ===
'admin' ?
'border-primary bg-primary-subtle' :
'bg-body-tertiary'),
393 'message_icon' => $internal ?
'bi-lock' : ($authorType ===
'admin' ?
'bi-headset' : ($authorType ===
'requester' ?
'bi-person' :
'bi-gear')),
394 'author_label' => $internal
395 ?
$texts->get_fd_message(
'timeline_internal_note')
396 : ($authorType ===
'admin'
397 ?
$texts->get_fd_message(
'timeline_support')
398 : ($authorType ===
'requester'
399 ?
$texts->get_fd_message(
'timeline_requester')
400 :
$texts->get_fd_message(
'timeline_system'))),
401 'create_date' => $this->h($message[
'create_date'] ??
''),
402 'body' => nl2br($this->h($message[
'body'] ??
'')),
403 'visibility_badge' => $internal
404 ?
'<span class="badge text-bg-warning">' . $this->h(
$texts->get_fd_message(
'timeline_internal_only')) .
'</span>'
406 'status_badge' => $statusTo !==
''
407 ?
'<span class="badge ' . $this->statusClass($statusTo) .
'">'
408 . $this->h(
$texts->format_fd_message(
410 array(
'status' =>
$texts->get_fd_message(
'status_' . $statusTo, $statusTo))
414 'mail_badge' => (
int) ($message[
'mail_sent'] ?? 0) === 1
415 ?
'<span class="badge text-bg-success"><i class="bi bi-envelope-check"></i> '
416 . $this->h(
$texts->get_fd_message(
'timeline_mail_sent'))
424 :
'<div class="text-muted">' . $this->h(
$texts->get_fd_message(
'timeline_empty')) .
'</div>';
427 private function mailProfileOptions(array $extra = array()): array {
428 $profile = trim((string)
dbx()->get_config(
'dbxContact',
'mail_profile'));
435 private function sendReplyMail(array $ticket,
string $body): bool {
436 $to = trim((string) ($ticket[
'email'] ??
''));
441 $from = trim((
string)
dbx()->get_config(
'dbxContact',
'mail_from'));
442 $fromName = trim((
string)
dbx()->get_config(
'dbxContact',
'mail_from_name'));
443 if (filter_var($from, FILTER_VALIDATE_EMAIL) ===
false) {
446 $fromParam = array(
'email' => $from,
'name' => $fromName);
447 $subject =
'Antwort zu Ticket #' . (int) $ticket[
'id'] .
': ' . (
string) ($ticket[
'subject'] ??
'Kontaktanfrage');
448 $html = $this->tpl()->get_tpl(
'dbxContact|mail-contact-reply', array(
449 'subject' => $this->h($ticket[
'subject'] ??
''),
450 'body' => nl2br($this->h(
$body)),
452 $text =
"Antwort zu Ticket #" . (int) $ticket[
'id'] .
"\n\n" .
$body;
454 return (
bool)
dbx()->send_mail(
461 $this->mailProfileOptions(array(
'text' => $text))
465 private function replyForm(array $ticket): string {
466 $rid = (int) $ticket[
'id'];
467 $form =
dbx()->get_system_obj(
'dbxForm');
468 $form->init(
'contact-ticket-reply',
'ticket-reply-form');
469 $form->_dd = dbxContactTicket::DD_MESSAGE;
470 $form->_fd =
'dbxContact_admin|ticket-reply';
471 $form->load_fd_messages();
472 $form->load_fd_messages(
473 'dbxContact_admin|rpt-ticket-selection'
475 $form->_action =
'?dbx_modul=dbxContact_admin&dbx_run1=ticket&rid=' . $rid;
476 $form->_data = array(
477 'status' => dbxContactTicket::normalizeStatus((
string) ($ticket[
'status'] ??
'open')),
478 'priority' => dbxContactTicket::normalizePriority((
string) ($ticket[
'priority'] ??
'normal')),
479 'visibility' =>
'public',
483 $form->add_module_bar(
484 $form->get_fd_message(
'bar_title'),
486 $form->get_fd_message(
'bar_subtitle')
488 $form->add_rep(
'bar_title_pre',
'');
489 $form->add_rep(
'bar_title_heading_attrs',
'');
490 $form->add_rep(
'bar_middle',
'');
491 $form->add_rep(
'bar_actions',
'');
492 $form->add_rep(
'bar_extra',
'');
493 $form->add_rep(
'rid', $rid);
494 $messageCount = max(0, (
int) $this->db()->count(dbxContactTicket::DD_MESSAGE, array(
'ticket_id' => $rid)));
495 $confirm = $this->deleteConfirmText(
503 '?dbx_modul=dbxContact_admin&dbx_run1=delete&rid=' . $rid
509 $form->_msg_info =
$form->get_fd_message(
'form_info');
510 $form->_msg_error =
$form->get_fd_message(
'validation_error');
511 $form->add_fld(
'status');
512 $form->add_fld(
'priority');
513 $form->add_fld(
'visibility');
514 $form->add_fld(
'body');
515 $form->add_fld(
'send_mail');
518 $status = dbxContactTicket::normalizeStatus((
string)
$form->get_post(
'status',
'answered',
'parameter|max=24'));
519 $priority = dbxContactTicket::normalizePriority((
string)
$form->get_post(
'priority',
'normal',
'parameter|max=16'));
520 $visibility = (string)
$form->get_post(
'visibility',
'public',
'parameter|max=16') ===
'internal' ?
'internal' :
'public';
521 $body = trim((
string)
$form->get_post_data(
'body',
'',
'*'));
522 $sendMail = (int)
$form->get_post(
'send_mail', 0,
'int') === 1 && $visibility ===
'public' && dbxContactConfig::mailOnReply();
523 $oldStatus = dbxContactTicket::normalizeStatus((
string) ($ticket[
'status'] ??
'open'));
526 $messageId = dbxContactTicket::addMessage(
$db, $rid, array(
527 'author_uid' => (
int)
dbx()->
user(),
528 'author_type' =>
'admin',
529 'message_type' => $visibility ===
'internal' ?
'note' :
'reply',
530 'visibility' => $visibility,
532 'status_from' => $oldStatus,
533 'status_to' => $status,
536 if ($messageId > 0) {
539 'priority' => $priority,
540 'assigned_uid' => (
int)
dbx()->
user(),
541 'closed_date' => $status ===
'closed' ? date(
'Y-m-d H:i:s') :
'',
543 dbxContactTicket::touch(
$db, $rid, $values);
547 $mailOk = $this->sendReplyMail($ticket,
$body);
549 $db->update(dbxContactTicket::DD_MESSAGE, array(
551 'mail_sent_date' => date(
'Y-m-d H:i:s'),
552 ), $messageId, 0, 1, 1, 1);
556 if ($sendMail && !$mailOk) {
561 $form->_msg_success = $visibility ===
'internal'
562 ?
$form->get_fd_message(
'internal_success')
563 :
$form->get_fd_message(
'reply_success')
565 ?
$form->get_fd_message(
'mail_success_suffix')
568 $form->_data[
'body'] =
'';
570 $form->_msg_error =
$form->get_fd_message(
'message_error');
577 private function ticket(): string {
578 $rid = (int)
dbx()->get_modul_var(
'rid', 0,
'int');
579 $texts =
dbx()->get_system_obj(
'dbxForm');
580 $texts->init(
'contact-ticket-texts',
'dbx|form');
581 $texts->load_fd_messages(
'dbxContact_admin|rpt-ticket-selection');
582 $ticket = dbxContactTicket::ticket($this->db(), $rid);
587 $texts->get_fd_message(
'ticket_not_found')
592 $replyForm = $this->replyForm($ticket);
593 $ticket = dbxContactTicket::ticket($this->db(), $rid);
594 $status = dbxContactTicket::normalizeStatus((
string) ($ticket[
'status'] ??
'open'));
595 $priority = dbxContactTicket::normalizePriority((
string) ($ticket[
'priority'] ??
'normal'));
596 $content = $this->tpl()->get_tpl(
'dbxContact_admin|ticket-detail', array(
598 'subject' => $this->h($ticket[
'subject'] ??
''),
599 'name' => $this->h($ticket[
'name'] ??
''),
600 'email' => $this->h($ticket[
'email'] ??
''),
601 'email_attr' => $this->h($ticket[
'email'] ??
''),
602 'phone' => $this->h(trim((
string) ($ticket[
'phone'] ??
'')) ?:
'-'),
603 'create_date' => $this->h($ticket[
'create_date'] ??
''),
604 'uid' => (
int) ($ticket[
'uid'] ?? 0) > 0
605 ?
'#' . (
int) $ticket[
'uid']
606 :
$texts->get_fd_message(
'guest'),
607 'status_label' => $this->h(
$texts->get_fd_message(
'status_' . $status, $status)),
608 'status_class' => $this->statusClass($status),
609 'priority_label' => $this->h(
$texts->get_fd_message(
'priority_' . $priority, $priority)),
610 'priority_class' => $this->priorityClass($priority),
611 'user_visibility' => (
int) ($ticket[
'user_hidden'] ?? 0) === 1
612 ?
$texts->get_fd_message(
'user_hidden')
613 :
$texts->get_fd_message(
'user_visible'),
614 'timeline' => $this->timeline($ticket,
$texts),
615 'reply_form' => $replyForm,
618 return $this->frame(
$content,
'Ticket #' . $rid);
621 private function deleteTicket(): string {
622 $rid = (int)
dbx()->get_modul_var(
'rid', 0,
'int');
623 $withFrame = (int)
dbx()->get_system_var(
'dbx_ajax', 0,
'int') !== 1;
624 $texts =
dbx()->get_system_obj(
'dbxReport');
625 $texts->init(
'contact-delete-texts',
'dbx|report');
627 'dbxContact_admin|rpt-ticket-selection'
629 if ($this->deleteTicketRecord($rid)) {
630 return $this->listTickets(
631 $texts->format_fd_message(
640 return $this->listTickets(
642 $texts->format_fd_message(
651 $run =
dbx()->get_modul_var(
'dbx_run1',
'list',
'parameter');
652 if (
$run ===
'install') {
653 return $this->install();
655 if (!$this->schemaReady()) {
656 return $this->alert(
'warning',
'Das Ticket-Datenmodell ist noch nicht installiert.')
657 .
'<div class="p-3"><a class="btn btn-primary" href="?dbx_modul=dbxContact_admin&dbx_run1=install"><i class="bi bi-database-gear"></i> Jetzt installieren</a></div>';
659 if (
$run ===
'ticket' ||
$run ===
'reply') {
660 return $this->ticket();
662 if (
$run ===
'delete') {
663 return $this->deleteTicket();
665 return $this->listTickets();