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>
46 lines
2.1 KiB
JSON
46 lines
2.1 KiB
JSON
{
|
|
"name": "dashboard/ticketing",
|
|
"description": "Help desk ticketing snap-in for the Dashboard platform",
|
|
"version": "1.0.4",
|
|
"type": "library",
|
|
"require": {
|
|
"php": "^8.2",
|
|
"illuminate/support": "^11.0|^12.0|^13.0",
|
|
"inertiajs/inertia-laravel": "^2.0|^3.0"
|
|
},
|
|
"autoload": {
|
|
"psr-4": {
|
|
"Dashboard\\Ticketing\\": "src/",
|
|
"Dashboard\\Ticketing\\Database\\Seeders\\": "src/Database/Seeders/"
|
|
}
|
|
},
|
|
"extra": {
|
|
"laravel": {
|
|
"providers": ["Dashboard\\Ticketing\\TicketingServiceProvider"]
|
|
},
|
|
"dashboard": {
|
|
"tier": "optional",
|
|
"stability": "stable",
|
|
"nav_folder": {
|
|
"label": "Help Desk",
|
|
"icon": "ticket",
|
|
"sort_order": 40
|
|
},
|
|
"pages": [
|
|
{ "label": "All Tickets", "route_name": "ticketing.index", "icon": "ticket", "permission": "ticketing.manage", "sort_order": 1 },
|
|
{ "label": "My Tickets", "route_name": "ticketing.my-tickets", "icon": "inbox-stack", "permission": "ticketing.view", "sort_order": 2 },
|
|
{ "label": "Settings", "route_name": "ticketing.settings", "icon": "cog-6-tooth", "permission": "ticketing.settings","sort_order": 99 }
|
|
],
|
|
"permissions": [
|
|
{ "key": "ticketing.view", "label": "View My Tickets", "description": "Submit tickets and view own ticket history" },
|
|
{ "key": "ticketing.create", "label": "Create Tickets", "description": "Create tickets on behalf of other users" },
|
|
{ "key": "ticketing.manage", "label": "Manage Tickets", "description": "View, assign, and resolve all tickets" },
|
|
{ "key": "ticketing.settings", "label": "Manage Settings", "description": "Configure groups, priorities, and integrations" }
|
|
],
|
|
"seeders": [
|
|
"Dashboard\\Ticketing\\Database\\Seeders\\EmailTemplatesSeeder"
|
|
]
|
|
}
|
|
}
|
|
}
|