intial ticket fix partial delete fix

This commit is contained in:
Mark Gregory
2026-06-18 17:15:56 +01:00
parent 70cd7149aa
commit 5064365af2
7 changed files with 325 additions and 25 deletions
+46 -24
View File
@@ -9,6 +9,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
use Modules\Helpdesk\Models\AutomationRule;
use Modules\Helpdesk\Models\CannedResponse;
@@ -134,7 +135,7 @@ final class SettingsManager extends Component
$data = $this->guardDefaultColumn('helpdesk_statuses', $data);
if ((bool) ($data['is_default'] ?? false)) {
Status::query()->whereKeyNot($this->statusForm['id'])->update(['is_default' => false]);
$this->clearOtherDefaults(Status::class, $this->statusForm['id']);
}
$this->persistSettingModel(Status::class, $this->statusForm['id'], $data);
@@ -173,7 +174,7 @@ final class SettingsManager extends Component
$data = $this->guardDefaultColumn('helpdesk_priorities', $data);
if ((bool) ($data['is_default'] ?? false)) {
Priority::query()->whereKeyNot($this->priorityForm['id'])->update(['is_default' => false]);
$this->clearOtherDefaults(Priority::class, $this->priorityForm['id']);
}
$this->persistSettingModel(Priority::class, $this->priorityForm['id'], $data);
$this->resetPriorityForm();
@@ -214,10 +215,7 @@ final class SettingsManager extends Component
$data = $this->guardDefaultColumn('helpdesk_categories', $data);
if ((bool) ($data['is_default'] ?? false)) {
Category::query()
->whereKeyNot($this->categoryForm['id'])
->where('company_id', $data['company_id'])
->update(['is_default' => false]);
$this->clearOtherDefaults(Category::class, $this->categoryForm['id'], $data['company_id'] ?? null);
}
$this->persistSettingModel(Category::class, $this->categoryForm['id'], $data);
$this->resetCategoryForm();
@@ -258,10 +256,7 @@ final class SettingsManager extends Component
$data = $this->guardDefaultColumn('helpdesk_teams', $data);
if ((bool) ($data['is_default'] ?? false)) {
Team::query()
->whereKeyNot($this->teamForm['id'])
->where('company_id', $data['company_id'])
->update(['is_default' => false]);
$this->clearOtherDefaults(Team::class, $this->teamForm['id'], $data['company_id'] ?? null);
}
$this->persistSettingModel(Team::class, $this->teamForm['id'], $data);
$this->resetTeamForm();
@@ -303,10 +298,7 @@ final class SettingsManager extends Component
$data = $this->guardDefaultColumn('helpdesk_projects', $data);
if ((bool) ($data['is_default'] ?? false)) {
Project::query()
->whereKeyNot($this->projectForm['id'])
->where('company_id', $data['company_id'])
->update(['is_default' => false]);
$this->clearOtherDefaults(Project::class, $this->projectForm['id'], $data['company_id'] ?? null);
}
$this->persistSettingModel(Project::class, $this->projectForm['id'], $data);
$this->resetProjectForm();
@@ -352,7 +344,7 @@ final class SettingsManager extends Component
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
if ((bool) ($data['is_default'] ?? false)) {
KanbanBoard::query()->whereKeyNot($this->boardForm['id'])->update(['is_default' => false]);
$this->clearOtherDefaults(KanbanBoard::class, $this->boardForm['id']);
}
$board = $this->persistSettingModel(KanbanBoard::class, $this->boardForm['id'], $data);
@@ -530,7 +522,7 @@ final class SettingsManager extends Component
])['slaForm'];
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
if ((bool) ($data['is_default'] ?? false)) {
SlaPolicy::query()->whereKeyNot($this->slaForm['id'])->update(['is_default' => false]);
$this->clearOtherDefaults(SlaPolicy::class, $this->slaForm['id']);
}
$this->persistSettingModel(SlaPolicy::class, $this->slaForm['id'], $data);
$this->resetSlaPolicyForm();
@@ -797,6 +789,7 @@ final class SettingsManager extends Component
private function orderedCategories(): mixed
{
return Category::query()
->with('company')
->withCount('tickets')
->when($this->hasDefaultColumn('helpdesk_categories'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name')
@@ -806,6 +799,7 @@ final class SettingsManager extends Component
private function orderedTeams(): mixed
{
return Team::query()
->with('company')
->withCount(['projects', 'tickets'])
->when($this->hasDefaultColumn('helpdesk_teams'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name')
@@ -815,7 +809,7 @@ final class SettingsManager extends Component
private function orderedProjects(): mixed
{
return Project::query()
->with(['team'])
->with(['company', 'team'])
->withCount('tickets')
->when($this->hasDefaultColumn('helpdesk_projects'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('title')
@@ -837,19 +831,47 @@ final class SettingsManager extends Component
*/
private function persistSettingModel(string $modelClass, int|string|null $id, array $data): object
{
if (filled($id)) {
unset($data['id']);
try {
if (filled($id)) {
/** @var TModel $model */
$model = $modelClass::query()->findOrFail((int) $id);
$model->fill($data);
$model->save();
return $model;
}
/** @var TModel $model */
$model = $modelClass::query()->findOrFail((int) $id);
$model->fill($data);
$model->save();
$model = $modelClass::query()->create($data);
return $model;
} catch (\Illuminate\Database\QueryException $exception) {
throw ValidationException::withMessages([
'settings' => 'The record could not be saved. Check the slug is unique for this section/company and try again.',
]);
}
}
/**
* Clear the default flag on every other record without generating invalid null-id SQL.
*
* @param class-string<\Illuminate\Database\Eloquent\Model> $modelClass
*/
private function clearOtherDefaults(string $modelClass, int|string|null $currentId, int|string|null $companyId = null): void
{
$query = $modelClass::query();
if (filled($currentId)) {
$query->whereKeyNot((int) $currentId);
}
/** @var TModel $model */
$model = $modelClass::query()->create($data);
if (Schema::hasColumn((new $modelClass())->getTable(), 'company_id')) {
$query->where('company_id', filled($companyId) ? (int) $companyId : null);
}
return $model;
$query->update(['is_default' => false]);
}
/**
+7
View File
@@ -7,6 +7,7 @@ namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -22,6 +23,12 @@ final class Category extends Model
/** @var array<string, string> */
protected $casts = ['company_id' => 'integer', 'is_default' => 'boolean'];
/** @return BelongsTo<\App\Models\Company, Category> */
public function company(): BelongsTo
{
return $this->belongsTo(\App\Models\Company::class, 'company_id');
}
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
+6
View File
@@ -23,6 +23,12 @@ final class Project extends Model
/** @var array<string, string> */
protected $casts = ['company_id' => 'integer', 'team_id' => 'integer', 'archived_at' => 'datetime', 'is_default' => 'boolean'];
/** @return BelongsTo<\App\Models\Company, Project> */
public function company(): BelongsTo
{
return $this->belongsTo(\App\Models\Company::class, 'company_id');
}
/** @return BelongsTo<Team, Project> */
public function team(): BelongsTo
{
+7
View File
@@ -6,6 +6,7 @@ namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -26,6 +27,12 @@ final class Team extends Model
'is_default' => 'boolean',
];
/** @return BelongsTo<\App\Models\Company, Team> */
public function company(): BelongsTo
{
return $this->belongsTo(\App\Models\Company::class, 'company_id');
}
/** @return HasMany<Project> */
public function projects(): HasMany
{
@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Modules\Helpdesk\Livewire\SettingsManager;
use Modules\Helpdesk\Models\AutomationRule;
use Modules\Helpdesk\Models\CannedResponse;
use Modules\Helpdesk\Models\Category;
use Modules\Helpdesk\Models\ChecklistTemplate;
use Modules\Helpdesk\Models\GitRepository;
use Modules\Helpdesk\Models\KanbanBoard;
use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\Project;
use Modules\Helpdesk\Models\SlaPolicy;
use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\Tag;
use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Models\WorkflowTemplate;
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
use Tests\TestCase;
final class SettingsCrudTest extends TestCase
{
use BuildsHelpdeskFixtures;
use RefreshDatabase;
public function test_all_helpdesk_settings_create_and_update_actions_persist_records(): void
{
$fixtures = $this->seedLookupData();
$admin = $this->admin(['name' => 'Settings CRUD Admin']);
$this->actingAs($admin);
Livewire::test(SettingsManager::class)
->set('statusForm.name', 'Awaiting Customer')
->set('statusForm.slug', 'awaiting-customer')
->set('statusForm.color', 'amber')
->set('statusForm.sort_order', 20)
->call('saveStatus')
->assertHasNoErrors();
$status = Status::query()->where('slug', 'awaiting-customer')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editStatus', $status->id)
->set('statusForm.name', 'Awaiting Customer Reply')
->call('saveStatus')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_statuses', ['id' => $status->id, 'name' => 'Awaiting Customer Reply']);
Livewire::test(SettingsManager::class)
->set('priorityForm.name', 'Critical')
->set('priorityForm.slug', 'critical')
->set('priorityForm.color', 'red')
->set('priorityForm.sort_order', 1)
->call('savePriority')
->assertHasNoErrors();
$priority = Priority::query()->where('slug', 'critical')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editPriority', $priority->id)
->set('priorityForm.name', 'Critical Updated')
->call('savePriority')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_priorities', ['id' => $priority->id, 'name' => 'Critical Updated']);
Livewire::test(SettingsManager::class)
->set('categoryForm.name', 'Billing')
->set('categoryForm.slug', 'billing')
->set('categoryForm.color', 'blue')
->call('saveCategory')
->assertHasNoErrors();
$category = Category::query()->where('slug', 'billing')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editCategory', $category->id)
->set('categoryForm.name', 'Billing Updated')
->call('saveCategory')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_categories', ['id' => $category->id, 'name' => 'Billing Updated']);
Livewire::test(SettingsManager::class)
->set('teamForm.name', 'Second Line')
->set('teamForm.slug', 'second-line')
->set('teamForm.color', 'indigo')
->call('saveTeam')
->assertHasNoErrors();
$team = Team::query()->where('slug', 'second-line')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editTeam', $team->id)
->set('teamForm.name', 'Second Line Updated')
->call('saveTeam')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_teams', ['id' => $team->id, 'name' => 'Second Line Updated']);
Livewire::test(SettingsManager::class)
->set('projectForm.team_id', $fixtures['team']->id)
->set('projectForm.title', 'Customer Portal')
->set('projectForm.slug', 'customer-portal')
->set('projectForm.color', 'cyan')
->call('saveProject')
->assertHasNoErrors();
$project = Project::query()->where('slug', 'customer-portal')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editProject', $project->id)
->set('projectForm.title', 'Customer Portal Updated')
->call('saveProject')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_projects', ['id' => $project->id, 'title' => 'Customer Portal Updated']);
Livewire::test(SettingsManager::class)
->set('boardForm.name', 'Escalations')
->set('boardForm.slug', 'escalations')
->set('boardForm.sort_order', 30)
->set('boardForm.status_ids', [$fixtures['open']->id, $fixtures['closed']->id])
->call('saveBoard')
->assertHasNoErrors();
$board = KanbanBoard::query()->where('slug', 'escalations')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editBoard', $board->id)
->set('boardForm.name', 'Escalations Updated')
->call('saveBoard')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_kanban_boards', ['id' => $board->id, 'name' => 'Escalations Updated']);
Livewire::test(SettingsManager::class)
->set('repositoryForm.project_id', $fixtures['project']->id)
->set('repositoryForm.name', 'WMS Core Repo')
->set('repositoryForm.provider', 'github')
->set('repositoryForm.remote_url', 'git@example.com:wms/core.git')
->set('repositoryForm.default_branch', 'main')
->call('saveRepository')
->assertHasNoErrors();
$repository = GitRepository::query()->where('name', 'WMS Core Repo')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editRepository', $repository->id)
->set('repositoryForm.default_branch', 'develop')
->call('saveRepository')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_git_repositories', ['id' => $repository->id, 'default_branch' => 'develop']);
Livewire::test(SettingsManager::class)
->set('workflowForm.name', 'Ticket Created')
->set('workflowForm.event', 'ticket.created')
->set('workflowForm.subject', 'New ticket')
->set('workflowForm.html_body', '<p>Created</p>')
->set('workflowForm.to_role', 'requester')
->call('saveWorkflowTemplate')
->assertHasNoErrors();
$workflow = WorkflowTemplate::query()->where('name', 'Ticket Created')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editWorkflowTemplate', $workflow->id)
->set('workflowForm.subject', 'New ticket updated')
->call('saveWorkflowTemplate')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_workflow_templates', ['id' => $workflow->id, 'subject' => 'New ticket updated']);
Livewire::test(SettingsManager::class)
->set('cannedForm.category_id', $fixtures['category']->id)
->set('cannedForm.team_id', $fixtures['team']->id)
->set('cannedForm.name', 'Greeting')
->set('cannedForm.slug', 'greeting')
->set('cannedForm.body', '<p>Hello</p>')
->call('saveCannedResponse')
->assertHasNoErrors();
$canned = CannedResponse::query()->where('slug', 'greeting')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editCannedResponse', $canned->id)
->set('cannedForm.body', '<p>Hello updated</p>')
->call('saveCannedResponse')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_canned_responses', ['id' => $canned->id, 'body' => '<p>Hello updated</p>']);
Livewire::test(SettingsManager::class)
->set('slaForm.priority_id', $fixtures['normal']->id)
->set('slaForm.category_id', $fixtures['category']->id)
->set('slaForm.name', 'Standard SLA')
->set('slaForm.slug', 'standard-sla')
->set('slaForm.first_response_minutes', 120)
->set('slaForm.resolution_minutes', 1440)
->call('saveSlaPolicy')
->assertHasNoErrors();
$sla = SlaPolicy::query()->where('slug', 'standard-sla')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editSlaPolicy', $sla->id)
->set('slaForm.resolution_minutes', 720)
->call('saveSlaPolicy')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_sla_policies', ['id' => $sla->id, 'resolution_minutes' => 720]);
Livewire::test(SettingsManager::class)
->set('automationForm.name', 'Auto Route')
->set('automationForm.slug', 'auto-route')
->set('automationForm.set_priority_id', $fixtures['high']->id)
->set('automationForm.set_status_id', $fixtures['open']->id)
->call('saveAutomationRule')
->assertHasNoErrors();
$automation = AutomationRule::query()->where('slug', 'auto-route')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editAutomationRule', $automation->id)
->set('automationForm.match_subject', 'urgent')
->call('saveAutomationRule')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_automation_rules', ['id' => $automation->id, 'match_subject' => 'urgent']);
Livewire::test(SettingsManager::class)
->set('checklistTemplateForm.category_id', $fixtures['category']->id)
->set('checklistTemplateForm.project_id', $fixtures['project']->id)
->set('checklistTemplateForm.name', 'Release Checklist')
->set('checklistTemplateForm.slug', 'release-checklist')
->set('checklistTemplateForm.items_text', "Check logs\nConfirm release")
->call('saveChecklistTemplate')
->assertHasNoErrors();
$checklist = ChecklistTemplate::query()->where('slug', 'release-checklist')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editChecklistTemplate', $checklist->id)
->set('checklistTemplateForm.items_text', "Updated item")
->call('saveChecklistTemplate')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_checklist_templates', ['id' => $checklist->id]);
self::assertSame(['Updated item'], $checklist->refresh()->items);
Livewire::test(SettingsManager::class)
->set('tagForm.name', 'VIP')
->set('tagForm.slug', 'vip')
->set('tagForm.color', 'pink')
->call('saveTag')
->assertHasNoErrors();
$tag = Tag::query()->where('slug', 'vip')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editTag', $tag->id)
->set('tagForm.color', 'purple')
->call('saveTag')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_tags', ['id' => $tag->id, 'color' => 'purple']);
Livewire::test(SettingsManager::class)
->set('productForm.name', 'WMS')
->set('productForm.slug', 'wms')
->set('productForm.type', 'service')
->call('saveProduct')
->assertHasNoErrors();
$product = Product::query()->where('slug', 'wms')->firstOrFail();
Livewire::test(SettingsManager::class)
->call('editProduct', $product->id)
->set('productForm.version', '1.1')
->call('saveProduct')
->assertHasNoErrors();
$this->assertDatabaseHas('helpdesk_products', ['id' => $product->id, 'version' => '1.1']);
}
}
+7
View File
@@ -1,3 +1,10 @@
- version: 1.9.29
date: 2026-06-18
changes:
- Hardened every Helpdesk settings create/edit/save path so Livewire CRUD actions persist updates without null primary-key inserts or invalid default-reset queries.
- Added company relationships/eager loading for category, team and project settings tables so settings pages render cleanly during edit/save cycles.
- Added full SettingsCrudTest coverage for statuses, priorities, categories, teams, projects, boards, Git repositories, workflow templates, canned responses, SLA policies, automation rules, checklist templates, tags and products.
- version: 1.3.6
date: 2026-06-16
changes:
+1 -1
View File
@@ -22,5 +22,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.9.28"
"version": "1.9.29"
}