1
0
mirror of git://f0xx.org/ac/ac-be-tickets synced 2026-07-29 01:38:26 +03:00
Files
ac-be-tickets/views/ticket_detail.php
Anton Afanasyeu 1d26b61111 initial
2026-06-23 12:29:35 +02:00

329 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/** Ticket detail — layout mirrors {@see report_detail.php} with workflow fields. */
$p = is_array($ticket['payload'] ?? null) ? $ticket['payload'] : [];
$device = is_array($p['device'] ?? null) ? $p['device'] : [];
$app = is_array($p['app'] ?? null) ? $p['app'] : [];
$pageTitle = 'Ticket';
$view = 'ticket';
$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
$rawJson = safe_json_encode($p, $jsonFlags);
$bp = Auth::basePath();
$ticketDbId = (int) ($ticket['id'] ?? 0);
$canEdit = !empty($ticket['can_edit']);
$deviceName = device_display_name($device);
$sdk = (int) ($device['sdk_int'] ?? 0);
$release = (string) ($device['release'] ?? '');
$abiList = [];
if (!empty($device['abis']) && is_array($device['abis'])) {
foreach ($device['abis'] as $abi) {
if (is_scalar($abi) && trim((string) $abi) !== '') {
$abiList[] = trim((string) $abi);
}
}
}
$rating = (int) ($ticket['rating'] ?? 0);
$workflowState = TicketWorkflow::normalize((string) ($ticket['workflow_state'] ?? 'triage'));
$assignees = is_array($ticket['assignees'] ?? null) ? $ticket['assignees'] : [];
$comments = is_array($ticket['comments'] ?? null) ? $ticket['comments'] : [];
$attachments = is_array($ticket['attachments'] ?? null) ? $ticket['attachments'] : [];
$canAttach = (bool) Auth::user();
$starsHtml = '';
for ($i = 1; $i <= 5; $i++) {
$starsHtml .= '<span class="star' . ($i <= $rating ? ' star--on' : '') . '" aria-hidden="true"></span>';
}
?>
<div class="detail-header">
<a href="<?= h($bp) ?>/?view=tickets" class="back-link" data-i18n-aria="ticket.back" data-i18n-title="ticket.back"
aria-label="Back to tickets list" title="Back to list">
<span class="back-icon" aria-hidden="true"></span>
</a>
<div>
<h1 data-i18n="ticket.detail_title">Ticket</h1>
<p class="muted ticket-detail-meta">
<span data-i18n="ticket.id_label">ID</span> <?= h($ticket['ticket_id'] ?? '') ?>
<?php if (!empty($ticket['brief'])): ?>
· <?= h($ticket['brief']) ?>
<?php endif; ?>
</p>
</div>
</div>
<section class="tag-editor" id="tag-editor" data-entity-type="ticket" data-ticket-id="<?= $ticketDbId ?>">
<div class="tag-editor-head">
<h2 data-i18n="detail.tags">Tags</h2>
<p class="muted tag-editor-hint" data-i18n="ticket.tag_hint">Include one type tag (<strong>ticket</strong> or <strong>issue</strong>) and a status tag (open, in progress, closed, …). Swap type via suggestions — remove the current type tag, then add the other.</p>
</div>
<div class="tag-editor-preview" id="tag-editor-preview" aria-live="polite"></div>
<?php if ($canEdit): ?>
<ul class="tag-editor-list" id="tag-editor-list"></ul>
<form class="tag-editor-add" id="tag-editor-add">
<label class="tag-field">
<span data-i18n="tag.label">Label</span>
<input type="text" name="label" maxlength="40" required data-i18n-placeholder="tag.placeholder" placeholder="e.g. alpha">
</label>
<label class="tag-field">
<span data-i18n="tag.color">Color</span>
<input type="color" name="bg" value="#5c6b82">
</label>
<button type="submit" class="btn" data-i18n="tag.add_tag">Add tag</button>
</form>
<div class="tag-preset-bar" id="tag-preset-bar" hidden>
<span class="muted" data-i18n="tag.suggestions">Suggestions:</span>
<div class="tag-preset-chips" id="tag-preset-chips"></div>
</div>
<div class="tag-editor-actions">
<button type="button" class="btn btn-primary" id="tag-editor-save" data-i18n="tag.save_tags">Save tags</button>
<span class="tag-editor-status muted" id="tag-editor-status"></span>
</div>
<?php else: ?>
<p class="muted" data-i18n="ticket.tag_readonly">Only the owner or an admin can edit tags.</p>
<?php endif; ?>
</section>
<script type="application/json" id="ticket-custom-tags-data"><?=
safe_json_encode($ticket['tags'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
?></script>
<?php if ($canEdit): ?>
<form class="ticket-edit-form cards cards--lift" id="ticket-edit-form" data-ticket-id="<?= $ticketDbId ?>">
<div class="card card--lift card--wide">
<h3 data-i18n="ticket.issue">Issue</h3>
<label class="ticket-field">
<span data-i18n="ticket.caption">Caption</span>
<input type="text" name="title" maxlength="256" required value="<?= h($ticket['title'] ?? '') ?>">
</label>
<label class="ticket-field">
<span data-i18n="ticket.summary">Summary</span>
<input type="text" name="brief" maxlength="512" value="<?= h($ticket['brief'] ?? '') ?>">
</label>
<label class="ticket-field">
<span data-i18n="ticket.description">Description</span>
<textarea name="body" rows="12"><?= h($ticket['body'] ?? '') ?></textarea>
</label>
</div>
<div class="card card--lift">
<h3 data-i18n="ticket.workflow">Workflow</h3>
<label class="ticket-field">
<span data-i18n="ticket.owner">Owner</span>
<input type="text" name="owner_username" maxlength="64" value="<?= h($ticket['owner_username'] ?? '') ?>">
</label>
<label class="ticket-field">
<span data-i18n="ticket.verified_by">Verified by</span>
<input type="text" name="verified_by_username" maxlength="64"
value="<?= h($ticket['verified_by_username'] ?? '') ?>">
</label>
<label class="ticket-field">
<span data-i18n="ticket.lifecycle">Lifecycle</span>
<select name="workflow_state" id="ticket-workflow-state">
<?php foreach (TicketWorkflow::all() as $ws): ?>
<option value="<?= h($ws) ?>"<?= $workflowState === $ws ? ' selected' : '' ?>>
<?= h(TicketWorkflow::labelDef($ws)['label']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<div class="ticket-field ticket-assignees-field">
<span data-i18n="ticket.assignees">Assignees</span>
<div class="assignee-editor" id="assignee-editor" aria-live="polite"></div>
<div class="assignee-add-row">
<select id="assignee-user-pick" aria-label="Add assignee">
<option value="">—</option>
</select>
<button type="button" class="btn" id="assignee-add-btn" data-i18n="ticket.assignee_add">Add</button>
</div>
</div>
<label class="ticket-field">
<span data-i18n="col.rating">Rating</span>
<select name="rating">
<?php for ($r = 0; $r <= 5; $r++): ?>
<option value="<?= $r ?>"<?= $rating === $r ? ' selected' : '' ?>><?= $r ?> ★</option>
<?php endfor; ?>
</select>
</label>
<p class="ticket-stars-preview">
<span class="star-rating" role="img" aria-label="<?= $rating ?> out of 5"><?= $starsHtml ?></span>
</p>
<div class="tag-editor-actions">
<button type="submit" class="btn btn-primary" data-i18n="ticket.save">Save ticket</button>
<span class="ticket-save-status muted" id="ticket-save-status"></span>
</div>
</div>
</form>
<script type="application/json" id="ticket-assignees-data"><?=
safe_json_encode($assignees, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
?></script>
<?php else: ?>
<section class="stack-block ticket-readonly-issue">
<h2><?= h($ticket['title'] ?? '') ?></h2>
<?php if (!empty($ticket['brief'])): ?>
<p class="muted"><?= h($ticket['brief']) ?></p>
<?php endif; ?>
<pre class="stack ticket-body"><?= h($ticket['body'] ?? '') ?></pre>
</section>
<?php endif; ?>
<div class="cards cards--lift">
<div class="card card--lift"><h3 data-i18n="detail.device">Device</h3>
<?php if ($deviceName !== ''): ?>
<p><?= h($deviceName) ?></p>
<?php else: ?>
<p class="muted">—</p>
<?php endif; ?>
<p>Android <?= h($release) ?><?= $sdk > 0 ? ' (SDK ' . $sdk . ')' : '' ?></p>
<?php if ($abiList !== []): ?>
<p class="muted detail-abi-row">ABIs: <?= h(implode(', ', $abiList)) ?></p>
<?php endif; ?>
</div>
<div class="card card--lift"><h3 data-i18n="detail.app">App</h3>
<p><?= h($app['package'] ?? $ticket['app_package'] ?? '') ?></p>
<p>v<?= h($app['version_name'] ?? $ticket['app_version'] ?? '') ?>
<?php if (!empty($app['version_code'])): ?>
<span class="muted">(<?= h((string) $app['version_code']) ?>)</span>
<?php endif; ?>
</p>
<p class="muted"><?= h($ticket['env_brief'] ?? '') ?></p>
</div>
<div class="card card--lift"><h3 data-i18n="ticket.timing">Timing</h3>
<p><span data-i18n="ticket.opened">Opened</span>:
<?= h(date('c', (int) (($ticket['opened_at_ms'] ?? 0) / 1000))) ?></p>
<p><span data-i18n="ticket.created">Created</span>:
<?= h(date('c', (int) (($ticket['created_at_ms'] ?? 0) / 1000))) ?></p>
<p><span data-i18n="ticket.updated">Updated</span>:
<?= h(date('c', (int) (($ticket['updated_at_ms'] ?? 0) / 1000))) ?></p>
</div>
<?php if (!$canEdit): ?>
<div class="card card--lift"><h3 data-i18n="ticket.workflow">Workflow</h3>
<p><span data-i18n="ticket.lifecycle">Lifecycle</span>:
<?= h(TicketWorkflow::labelDef($workflowState)['label']) ?></p>
<p><span data-i18n="ticket.assignees">Assignees</span>:
<?php if ($assignees === []): ?>
<span class="muted">—</span>
<?php else: ?>
<?php foreach ($assignees as $a): ?>
<span class="assignee-pill"><?= h($a['username']) ?><?php if (!empty($a['role'])): ?>
<span class="muted">(<?= h($a['role']) ?>)</span><?php endif; ?></span>
<?php endforeach; ?>
<?php endif; ?>
</p>
<p><span data-i18n="ticket.owner">Owner</span>: <?= h($ticket['owner_username'] ?? '—') ?></p>
<p><span data-i18n="ticket.verified_by">Verified by</span>:
<?= h($ticket['verified_by_username'] ?? '—') ?></p>
<p><span data-i18n="col.rating">Rating</span>:
<span class="star-rating" role="img"><?= $starsHtml ?></span></p>
</div>
<?php endif; ?>
</div>
<section class="ticket-attachments" id="ticket-attachments" data-ticket-id="<?= $ticketDbId ?>">
<h2 data-i18n="ticket.attachments">Attachments</h2>
<ul class="ticket-attachment-list" id="ticket-attachment-list">
<?php foreach ($attachments as $att): ?>
<li class="ticket-attachment ticket-attachment--<?= h($att['kind']) ?>"
data-attachment-id="<?= (int) ($att['id'] ?? 0) ?>">
<span class="attachment-provider"><?= h($att['provider_label'] ?? 'File') ?></span>
<?php if (($att['kind'] ?? '') === 'link' && !empty($att['external_url'])): ?>
<a href="<?= h($att['external_url']) ?>" target="_blank" rel="noopener noreferrer"><?= h($att['title'] ?: $att['external_url']) ?></a>
<?php elseif (!empty($att['download_url'])): ?>
<a href="<?= h($att['download_url']) ?>"><?= h($att['title'] ?: $att['original_name']) ?></a>
<?php if (!empty($att['size_bytes'])): ?>
<span class="muted">(<?= h(number_format((int) $att['size_bytes'] / 1024, 1)) ?> KB)</span>
<?php endif; ?>
<?php else: ?>
<span><?= h($att['title'] ?? '') ?></span>
<?php endif; ?>
<span class="muted attachment-meta"><?= h($att['created_by'] ?? '') ?></span>
<?php if ($canAttach): ?>
<button type="button" class="btn btn-small attachment-remove" data-id="<?= (int) ($att['id'] ?? 0) ?>">×</button>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php if ($canAttach): ?>
<div class="ticket-attachment-add cards cards--lift">
<form class="ticket-link-form" id="ticket-link-form">
<h3 data-i18n="ticket.attach_link">Add link</h3>
<p class="muted" data-i18n="ticket.attach_link_hint">Google Docs, Sheets, Drive, Meet, or any HTTPS URL.</p>
<label class="ticket-field">
<span data-i18n="ticket.link_title">Title (optional)</span>
<input type="text" name="title" maxlength="256">
</label>
<label class="ticket-field">
<span>URL</span>
<input type="url" name="url" required placeholder="https://docs.google.com/...">
</label>
<button type="submit" class="btn" data-i18n="ticket.attach_link_btn">Attach link</button>
<span class="muted" id="ticket-link-status"></span>
</form>
<form class="ticket-file-form" id="ticket-file-form" enctype="multipart/form-data">
<h3 data-i18n="ticket.attach_file">Upload file</h3>
<p class="muted" data-i18n="ticket.attach_file_hint">Max 16 MiB — images, logs, pdf, zip, …</p>
<label class="ticket-field">
<span data-i18n="ticket.link_title">Title (optional)</span>
<input type="text" name="title" maxlength="256">
</label>
<label class="ticket-field">
<span data-i18n="ticket.file_pick">File</span>
<input type="file" name="file" required>
</label>
<button type="submit" class="btn btn-primary" data-i18n="ticket.attach_file_btn">Upload</button>
<span class="muted" id="ticket-file-status"></span>
</form>
</div>
<?php endif; ?>
</section>
<section class="ticket-comments" id="ticket-comments" data-ticket-id="<?= $ticketDbId ?>">
<h2 data-i18n="ticket.comments">Comments</h2>
<ul class="ticket-comment-list" id="ticket-comment-list">
<?php foreach ($comments as $c): ?>
<li class="ticket-comment">
<div class="ticket-comment-meta">
<strong><?= h($c['author_username']) ?></strong>
<span class="muted"><?= h(date('c', (int) (($c['created_at_ms'] ?? 0) / 1000))) ?></span>
</div>
<pre class="ticket-comment-body"><?= h($c['body']) ?></pre>
</li>
<?php endforeach; ?>
</ul>
<?php if ($canEdit || Auth::user()): ?>
<form class="ticket-comment-form" id="ticket-comment-form">
<label class="ticket-field">
<span data-i18n="ticket.comment_add">Add comment</span>
<textarea name="body" rows="4" required></textarea>
</label>
<button type="submit" class="btn btn-primary" data-i18n="ticket.comment_post">Post</button>
<span class="muted" id="ticket-comment-status"></span>
</form>
<?php endif; ?>
</section>
<div class="detail-tree" id="detail-tree">
<?php if (!empty($p['source_file']) || !empty($p['alpha_block'])): ?>
<div class="detail-tree-item">
<div class="detail-tree-row report-row" role="button" tabindex="0" data-tree-toggle data-controls="detail-source">
<span class="report-tree-cell">
<button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="detail-source" tabindex="-1">
<span class="report-tree-arrow" aria-hidden="true"></span>
</button>
</span>
<span class="detail-tree-caption" data-i18n="ticket.source">Source / context</span>
</div>
<div class="detail-tree-body" id="detail-source" hidden>
<pre class="stack"><?= h((string) ($p['source_excerpt'] ?? $p['notes'] ?? '')) ?></pre>
</div>
</div>
<?php endif; ?>
<div class="detail-tree-item">
<div class="detail-tree-row report-row" role="button" tabindex="0" data-tree-toggle data-controls="detail-raw-json">
<span class="report-tree-cell">
<button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="detail-raw-json" tabindex="-1">
<span class="report-tree-arrow" aria-hidden="true"></span>
</button>
</span>
<span class="detail-tree-caption" data-i18n="detail.raw_json">Raw JSON</span>
</div>
<div class="detail-tree-body" id="detail-raw-json" hidden>
<pre class="stack raw-json"><?= h($rawJson) ?></pre>
</div>
</div>
</div>