Commit Graph

6 Commits

Author SHA1 Message Date
ef4d0c4f93 A system note is a ticket message source
Merge and Split both 500ed. The audit note each writes uses `source = 'system'`
and the column is `enum('web','email')`, so the insert threw inside the
transaction and nothing committed — which is why the operator saw no state change
at all rather than a half-done merge.

Widened to an enum rather than a varchar, and the reasoning is the same test that
made `tickets.status` a varchar in `2026_04_17_000002`: statuses became
operator-definable rows, so their domain is open. `source` is the opposite — all
three values are set by code, an operator can never add a fourth, and the enum is
what keeps a typo'd source loud instead of a silently unfilterable row.

`TicketMessage::system()` is now the only place a system note is written. Three
scattered literals is how this reopens.

A SECOND 500 ON THE SAME ACTION, unreported and one longer title away. `split()`
appended ' (split)' to a title the product validates at max:255, so splitting a
full-length title produced 263 characters and the identical hard failure. Bounded
at the write rather than widened, and the distinction is written down: widen when
the data is right and the column is wrong; bound when the column expresses a real
product rule. 255 is the product's own limit, so a 263-character title would be
un-editable forever — the edit form would bounce it every time.

And `split()` now honours its own comment. Line 682 has always said "exclude
system notes" while the code copied them; it never mattered because no system row
could exist. The moment the enum accepts them, splitting a merged ticket would
carry "Merged from ticket QAX-0005." onto a ticket that was never merged.
2026-08-29 11:09:42 -04:00
0e48949114 A ticket reference is never re-issued, and notifications name their recipient (#224, #228)
#224 — the next number came from the live rows, so deleting a ticket freed its
reference and two tickets could both be QAX-0004. An old email then points at a
different ticket than it did. The sequence is now held on the group and only ever
moves forward.

#228 — ticketing notifications carried no user, so the Notification Log showed
the message type where the recipient belongs and the per-person opt-out was
ignored: somebody who had opted out still received them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ec3REU4LQmhuchRRn6dZHk
2026-08-27 19:37:06 -04:00
1175c20a57 Ticketing groups can be retired (e2e dashboard-ticketing#7)
There was no destroy route at all — only settings.groups.store and .update — so
the e2e CRUD spec, which creates an ephemeral group as its documented setup step,
could never clean up (a DELETE-spoofed POST returned 405). Every sweep left litter
behind, and an operator who mistyped a group was stuck with it forever.

Deleting a group cannot be unconditional: tickets are the audit record of real
correspondence, and tickets.group_id is a RESTRICT FK. So destroyGroup counts the
tickets and picks:

  0 tickets  -> real delete (agent access / projects / email connections are
                cascadeOnDelete, so nothing dangles). This is the teardown and
                undo-a-typo path.
  >=1 ticket -> archived_at = now(). Tickets untouched and still readable.

The caller is TOLD which happened rather than inferring it from a 302: an
expectsJson caller gets {"outcome":"deleted"|"archived","ticket_count":N}, the UI
gets distinct flash wording, and the confirm modal says which it will be BEFORE
you click ("This group has 1 ticket and will be archived, not deleted").
Archiving is idempotent; restore is a separate route.

Archived groups leave the PICKERS but nothing else: index() and create() use
scopeActive, store() validation adds whereNull('archived_at') so a stale id cannot
bypass the picker, and update() rejects transfers INTO an archived group while
always allowing the ticket's own group — otherwise a ticket stranded in an
archived group would become uneditable. Index.vue re-adds that own group to the
transfer select labelled "(archived)" so the select is never blank. Deliberately
NOT filtered: the Settings groups list (archived rows show a badge + Restore), and
agentGroupIds()/isAgent()/isManager(), which read ticketing_agent_access — so
existing agents keep working the tickets that remain. No global scope; exclusion
is per call site.

Gating is byte-identical to updateGroup: inside the existing
permission:ticketing.settings group plus requireManagerAccess(), so a super admin
with no manager row on that group is still refused (asserted).

Migration is additive — one nullable timestamp. down() is a deliberate NO-OP,
matching the myedpuller-sync warnings-column precedent: DashboardDoctor greps the
whole file for drop/rename patterns, so a dropColumn even in down() would raise a
"pending destructive migration" advisory on every host where it is still pending,
which is a confusing warning for a purely additive change. Verified against
doctor's exact regex, not a looser grep.

Tests: 9 (69 assertions). 10 mutations, all 10 detected — including forcing a hard
delete on a group with tickets, which throws a QueryException from the RESTRICT FK,
i.e. the database backs up the application check.

Browser-verified on dev: the confirm modal wording for both cases, the ARCHIVED
badge and Restore, the archived group vanishing from both the create picker and the
All-Tickets switcher while its ticket stays listed, and ZERO native
confirm/alert dialogs. doctor exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 19:35:04 -04:00
Joel Wedemire
dd0a458250 feat: ticket views, statuses, participants, merge; mobile layout fixes (#5)
- New migrations: ticket views, statuses, participants, merge support
- New models: TicketView, TicketStatus, TicketParticipant
- New seeder: EmailTemplatesSeeder
- Console commands for ticketing
- Mobile: sidebar min-w-0/overflow-hidden, tab nav overflow-x-auto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 22:22:45 -07:00
Joel Wedemire
391699220f feat: full dashboard-ticketing scaffold with data model, controllers, Vue pages 2026-04-08 17:10:30 -07:00
Joel Wedemire
81d0d54f50 feat: initial dashboard-ticketing scaffold 2026-04-08 14:17:26 -07:00