mirror of
git://f0xx.org/ac/ac-be-tickets
synced 2026-07-29 04:58:24 +03:00
initial
This commit is contained in:
328
views/ticket_detail.php
Normal file
328
views/ticket_detail.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user