feat: duplicate link action, watcher migration, fulltext search migration

- CreateTicketView: "Link as duplicate" button on each duplicate result;
  stores chosen ticket ID in hidden field, auto-creates duplicates dependency
  after ticket is saved (TicketController)
- migrations/004: ticket_watchers table (ticket_id, user_id primary key)
- migrations/005: FULLTEXT index on tickets(title, description) for fast
  relevance search replacing LIKE scan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:54:00 -04:00
parent c8181e8076
commit 0acf5e84c3
4 changed files with 46 additions and 1 deletions
+19 -1
View File
@@ -37,6 +37,7 @@ include __DIR__ . '/layout_header.php';
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="link_duplicate_of" id="linkDuplicateOf" value="">
<?php if (isset($error)): ?>
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
@@ -269,6 +270,7 @@ include __DIR__ . '/layout_header.php';
ul.className = 'duplicate-list lt-text-sm';
data.duplicates.forEach(function (dup) {
var li = document.createElement('li');
li.className = 'lt-flex lt-flex-align-center lt-flex-gap-sm lt-mb-xs';
var a = document.createElement('a');
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
a.target = '_blank';
@@ -277,14 +279,30 @@ include __DIR__ . '/layout_header.php';
var badge = document.createElement('span');
badge.className = 'lt-text-muted';
badge.textContent = '(' + dup.similarity + '% match, ' + dup.status + ')';
var linkBtn = document.createElement('button');
linkBtn.type = 'button';
linkBtn.className = 'lt-btn lt-btn-ghost lt-btn-xs';
linkBtn.dataset.dupId = dup.ticket_id;
linkBtn.textContent = 'Link as duplicate';
linkBtn.title = 'After creating, this ticket will be linked as a duplicate of #' + dup.ticket_id;
linkBtn.addEventListener('click', function () {
var chosen = this.dataset.dupId;
document.getElementById('linkDuplicateOf').value = chosen;
// Update all buttons to show current selection
ul.querySelectorAll('[data-dup-id]').forEach(function (b) {
b.textContent = b.dataset.dupId === chosen ? '\u2713 Will link' : 'Link as duplicate';
b.classList.toggle('lt-btn-primary', b.dataset.dupId === chosen);
});
});
li.appendChild(a);
li.appendChild(dash);
li.appendChild(badge);
li.appendChild(linkBtn);
ul.appendChild(li);
});
var hint = document.createElement('p');
hint.className = 'lt-text-xs lt-text-muted lt-mt-sm';
hint.textContent = 'Check these before creating a new ticket.';
hint.textContent = 'Check these before creating. Use "Link as duplicate" to auto-link after create.';
list.innerHTML = '';
list.appendChild(ul);
list.appendChild(hint);