structure and style updates

This commit is contained in:
Mark Gregory
2026-06-16 23:21:49 +01:00
parent 80991208af
commit 2c2af4c50e
60 changed files with 209 additions and 216 deletions
+2 -3
View File
@@ -2,7 +2,6 @@
return [ return [
'name' => 'AuditTrail', 'name' => 'AuditTrail',
'features' => 'features' => [
[ ],
],
]; ];
+1
View File
@@ -1,4 +1,5 @@
<?php <?php
use Modules\AuditTrail\Support\ModuleInfo; use Modules\AuditTrail\Support\ModuleInfo;
if (! function_exists('audittrail_feature_enabled')) { if (! function_exists('audittrail_feature_enabled')) {
@@ -10,6 +10,7 @@ class AuditTrailController extends Controller
{ {
/** /**
* Display a listing of the resource. * Display a listing of the resource.
*
* @return Renderable * @return Renderable
*/ */
public function index() public function index()
@@ -19,6 +20,7 @@ class AuditTrailController extends Controller
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
*
* @return Renderable * @return Renderable
*/ */
public function create() public function create()
@@ -28,7 +30,7 @@ class AuditTrailController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* @param Request $request *
* @return Renderable * @return Renderable
*/ */
public function store(Request $request) public function store(Request $request)
@@ -38,7 +40,8 @@ class AuditTrailController extends Controller
/** /**
* Show the specified resource. * Show the specified resource.
* @param int $id *
* @param int $id
* @return Renderable * @return Renderable
*/ */
public function show($id) public function show($id)
@@ -48,7 +51,8 @@ class AuditTrailController extends Controller
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* @param int $id *
* @param int $id
* @return Renderable * @return Renderable
*/ */
public function edit($id) public function edit($id)
@@ -58,8 +62,8 @@ class AuditTrailController extends Controller
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* @param Request $request *
* @param int $id * @param int $id
* @return Renderable * @return Renderable
*/ */
public function update(Request $request, $id) public function update(Request $request, $id)
@@ -69,7 +73,8 @@ class AuditTrailController extends Controller
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* @param int $id *
* @param int $id
* @return Renderable * @return Renderable
*/ */
public function destroy($id) public function destroy($id)
@@ -2,41 +2,29 @@
namespace Modules\AuditTrail\Providers; namespace Modules\AuditTrail\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;
use Illuminate\Support\Facades\File;
use Modules\AuditTrail\Support\ModuleInfo;
use Config; use Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\ServiceProvider;
use Modules\AuditTrail\Support\ModuleInfo;
class AuditTrailServiceProvider extends ServiceProvider class AuditTrailServiceProvider extends ServiceProvider
{ {
/**
* @var string $moduleName
*/
protected string $moduleName = 'AuditTrail'; protected string $moduleName = 'AuditTrail';
/**
* @var string $moduleNameLower
*/
protected string $moduleNameLower = 'audittrail'; protected string $moduleNameLower = 'audittrail';
/**
/** * Get the services provided by the provider.
* Get the services provided by the provider. */
* public function provides(): array
* @return array
*/
public function provides():array
{ {
return []; return [];
} }
/** /**
* Boot the application events. * Boot the application events.
*
* @return void
*/ */
public function boot() : void public function boot(): void
{ {
$this->registerTranslations(); $this->registerTranslations();
$this->registerConfig(); $this->registerConfig();
@@ -48,8 +36,6 @@ class AuditTrailServiceProvider extends ServiceProvider
/** /**
* Register the service provider. * Register the service provider.
*
* @return void
*/ */
public function register(): void public function register(): void
{ {
@@ -58,8 +44,6 @@ class AuditTrailServiceProvider extends ServiceProvider
/** /**
* Register the helpers. * Register the helpers.
*
* @return void
*/ */
protected function registerHelpers(): void protected function registerHelpers(): void
{ {
@@ -68,71 +52,64 @@ class AuditTrailServiceProvider extends ServiceProvider
} }
} }
/** /**
* Dynamically Register Commands. * Dynamically Register Commands.
*
* @return void
*/ */
public function registerCommands(): void public function registerCommands(): void
{ {
if (!$this->app->runningInConsole()) { if (! $this->app->runningInConsole()) {
return; return;
}
$commandPath = __DIR__ . '/../Console';
$commands = collect(File::allFiles($commandPath))
->map(function ($file) {
$class = 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
return $class;
})
->filter(function ($class) {
return class_exists($class);
})
->values()
->all();
$this->commands($commands);
} }
$commandPath = __DIR__.'/../Console';
$commands = collect(File::allFiles($commandPath))
->map(function ($file) {
$class = 'Modules\\'.$this->moduleName.'\\Console\\'.$file->getFilenameWithoutExtension();
return $class;
})
->filter(function ($class) {
return class_exists($class);
})
->values()
->all();
$this->commands($commands);
}
/** /**
* Register config. * Register config.
*
* @return void
*/ */
protected function registerConfig(): void protected function registerConfig(): void
{ {
$this->publishes([ $this->publishes([
module_path(ModuleInfo::name(), 'Config/config.php') => config_path( ModuleInfo::nameLower(). '.php'), module_path(ModuleInfo::name(), 'Config/config.php') => config_path(ModuleInfo::nameLower().'.php'),
], 'config'); ], 'config');
$this->mergeConfigFrom( $this->mergeConfigFrom(
module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower() module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()
); );
} }
/** /**
* Register views. * Register views.
*
* @return void
*/ */
public function registerViews(): void public function registerViews(): void
{ {
$viewPath = resource_path('views/modules/' . ModuleInfo::nameLower()); $viewPath = resource_path('views/modules/'.ModuleInfo::nameLower());
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views'); $sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
$this->publishes([ $this->publishes([
$sourcePath => $viewPath $sourcePath => $viewPath,
], ['views', ModuleInfo::nameLower() . '-module-views']); ], ['views', ModuleInfo::nameLower().'-module-views']);
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower()); $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
} }
/** /**
* Register translations. * Register translations.
*
* @return void
*/ */
public function registerTranslations(): void public function registerTranslations(): void
{ {
$langPath = resource_path('lang/modules/' . ModuleInfo::nameLower()); $langPath = resource_path('lang/modules/'.ModuleInfo::nameLower());
if (is_dir($langPath)) { if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower()); $this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
} else { } else {
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower()); $this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
} }
@@ -140,16 +117,16 @@ class AuditTrailServiceProvider extends ServiceProvider
/** /**
* Regsiter View Paths * Regsiter View Paths
* @return array
*/ */
private function getPublishableViewPaths(): array private function getPublishableViewPaths(): array
{ {
$paths = []; $paths = [];
foreach (Config::get('view.paths') as $path) { foreach (Config::get('view.paths') as $path) {
if (is_dir($path . '/modules/' . ModuleInfo::nameLower())) { if (is_dir($path.'/modules/'.ModuleInfo::nameLower())) {
$paths[] = $path . '/modules/' . ModuleInfo::nameLower(); $paths[] = $path.'/modules/'.ModuleInfo::nameLower();
} }
} }
return $paths; return $paths;
} }
} }
@@ -2,8 +2,8 @@
namespace Modules\AuditTrail\Providers; namespace Modules\AuditTrail\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
+1 -1
View File
@@ -11,6 +11,6 @@
| |
*/ */
Route::prefix('audittrail')->group(function() { Route::prefix('audittrail')->group(function () {
Route::get('/', 'AuditTrailController@index'); Route::get('/', 'AuditTrailController@index');
}); });
+1 -3
View File
@@ -4,11 +4,10 @@ namespace Modules\AuditTrail\Support;
class ModuleInfo class ModuleInfo
{ {
protected static string $moduleName = 'AuditTrail'; protected static string $moduleName = 'AuditTrail';
/** /**
* defines the Module Name * defines the Module Name
* @return string
*/ */
public static function name(): string public static function name(): string
{ {
@@ -17,7 +16,6 @@ class ModuleInfo
/** /**
* define the lowercase version * define the lowercase version
* @return string
*/ */
public static function nameLower(): string public static function nameLower(): string
{ {
+1
View File
@@ -1,4 +1,5 @@
<?php <?php
use Modules\Countries\Support\ModuleInfo; use Modules\Countries\Support\ModuleInfo;
if (! function_exists('countries_feature_enabled')) { if (! function_exists('countries_feature_enabled')) {
@@ -57,7 +57,7 @@ class CountriesController extends Controller
*/ */
public function create(): View public function create(): View
{ {
return view('countries::create', ['country' => new Country()]); return view('countries::create', ['country' => new Country]);
} }
/** /**
@@ -112,7 +112,6 @@ class CountriesController extends Controller
/** /**
* Build the database payload from validated request data. * Build the database payload from validated request data.
* *
* @param StoreCountryRequest|UpdateCountryRequest $request
* @return array<string, mixed> * @return array<string, mixed>
*/ */
private function payload(StoreCountryRequest|UpdateCountryRequest $request): array private function payload(StoreCountryRequest|UpdateCountryRequest $request): array
@@ -16,7 +16,6 @@ class StoreCountryRequest extends FormRequest
return (bool) $this->user()?->isMpbaPlatformAdmin(); return (bool) $this->user()?->isMpbaPlatformAdmin();
} }
/** /**
* Normalise ISO code inputs before validation. * Normalise ISO code inputs before validation.
*/ */
@@ -16,7 +16,6 @@ class UpdateCountryRequest extends FormRequest
return (bool) $this->user()?->isMpbaPlatformAdmin(); return (bool) $this->user()?->isMpbaPlatformAdmin();
} }
/** /**
* Normalise ISO code inputs before validation. * Normalise ISO code inputs before validation.
*/ */
@@ -125,6 +125,7 @@ class CountriesServiceProvider extends ServiceProvider
if (is_dir($publishedPath)) { if (is_dir($publishedPath)) {
$this->loadTranslationsFrom($publishedPath, ModuleInfo::nameLower()); $this->loadTranslationsFrom($publishedPath, ModuleInfo::nameLower());
return; return;
} }
+1 -3
View File
@@ -7,9 +7,7 @@ use Illuminate\Contracts\Validation\ValidationRule;
class ValidCountryCode implements ValidationRule class ValidCountryCode implements ValidationRule
{ {
public function __construct(private readonly bool $activeOnly = true) public function __construct(private readonly bool $activeOnly = true) {}
{
}
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
@@ -11,13 +11,13 @@ class ValidIso3166CountryCode implements ValidationRule
public function __construct( public function __construct(
private readonly bool $activeOnly = true, private readonly bool $activeOnly = true,
private readonly string $format = 'alpha2', private readonly string $format = 'alpha2',
) { ) {}
}
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
if (! is_string($value) && ! is_int($value)) { if (! is_string($value) && ! is_int($value)) {
$fail('The :attribute must be a valid ISO 3166 country code.'); $fail('The :attribute must be a valid ISO 3166 country code.');
return; return;
} }
@@ -8,14 +8,13 @@ use Modules\Countries\Models\CountrySubdivision;
class ValidIso3166SubdivisionCode implements ValidationRule class ValidIso3166SubdivisionCode implements ValidationRule
{ {
public function __construct(private readonly bool $activeOnly = true) public function __construct(private readonly bool $activeOnly = true) {}
{
}
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
if (! is_string($value) || ! preg_match('/^[A-Z]{2}-[A-Z0-9]{1,6}$/', strtoupper($value))) { if (! is_string($value) || ! preg_match('/^[A-Z]{2}-[A-Z0-9]{1,6}$/', strtoupper($value))) {
$fail('The :attribute must be a valid ISO 3166-2 subdivision code.'); $fail('The :attribute must be a valid ISO 3166-2 subdivision code.');
return; return;
} }
+5
View File
@@ -7,10 +7,15 @@ use InvalidArgumentException;
final class Iso3166 final class Iso3166
{ {
public const STATUS_OFFICIAL = 'official'; public const STATUS_OFFICIAL = 'official';
public const STATUS_RESERVED = 'reserved'; public const STATUS_RESERVED = 'reserved';
public const STATUS_TRANSITIONAL = 'transitional'; public const STATUS_TRANSITIONAL = 'transitional';
public const STATUS_DELETED = 'deleted'; public const STATUS_DELETED = 'deleted';
public const STATUS_USER_ASSIGNED = 'user_assigned'; public const STATUS_USER_ASSIGNED = 'user_assigned';
public const STATUS_EXCEPTIONALLY_RESERVED = 'exceptionally_reserved'; public const STATUS_EXCEPTIONALLY_RESERVED = 'exceptionally_reserved';
/** @return list<string> */ /** @return list<string> */
+1 -3
View File
@@ -4,11 +4,10 @@ namespace Modules\Countries\Support;
class ModuleInfo class ModuleInfo
{ {
protected static string $moduleName = 'Countries'; protected static string $moduleName = 'Countries';
/** /**
* defines the Module Name * defines the Module Name
* @return string
*/ */
public static function name(): string public static function name(): string
{ {
@@ -17,7 +16,6 @@ class ModuleInfo
/** /**
* define the lowercase version * define the lowercase version
* @return string
*/ */
public static function nameLower(): string public static function nameLower(): string
{ {
@@ -4,8 +4,6 @@ namespace Modules\Countries\Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\Countries\Database\Seeders\CountriesDatabaseSeeder; use Modules\Countries\Database\Seeders\CountriesDatabaseSeeder;
use Modules\Countries\Models\Country;
use Modules\Countries\Models\CountrySubdivision;
use Modules\Countries\Rules\ValidIso3166CountryCode; use Modules\Countries\Rules\ValidIso3166CountryCode;
use Modules\Countries\Rules\ValidIso3166SubdivisionCode; use Modules\Countries\Rules\ValidIso3166SubdivisionCode;
use Tests\TestCase; use Tests\TestCase;
@@ -9,9 +9,9 @@ use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Modules\Documentation\Http\Requests\StoreDocumentationDocumentRequest; use Modules\Documentation\Http\Requests\StoreDocumentationDocumentRequest;
use Modules\Documentation\Http\Requests\UpdateDocumentationDocumentRequest; use Modules\Documentation\Http\Requests\UpdateDocumentationDocumentRequest;
use Modules\Documentation\Models\DocumentationAttachment;
use Modules\Documentation\Models\DocumentationCategory; use Modules\Documentation\Models\DocumentationCategory;
use Modules\Documentation\Models\DocumentationDocument; use Modules\Documentation\Models\DocumentationDocument;
use Modules\Documentation\Models\DocumentationAttachment;
class DocumentationController extends Controller class DocumentationController extends Controller
{ {
@@ -31,7 +31,7 @@ class DocumentationController extends Controller
} }
if ($request->filled('search')) { if ($request->filled('search')) {
$search = '%' . $request->string('search')->trim()->toString() . '%'; $search = '%'.$request->string('search')->trim()->toString().'%';
$query->where(function ($builder) use ($search): void { $query->where(function ($builder) use ($search): void {
$builder->where('title', 'like', $search) $builder->where('title', 'like', $search)
->orWhere('summary', 'like', $search) ->orWhere('summary', 'like', $search)
@@ -197,7 +197,6 @@ class DocumentationController extends Controller
return redirect()->route('documentation.documents.edit', $document)->with('status', 'Attachment removed successfully.'); return redirect()->route('documentation.documents.edit', $document)->with('status', 'Attachment removed successfully.');
} }
/** /**
* Extract body content from an uploaded Markdown file when requested. * Extract body content from an uploaded Markdown file when requested.
*/ */
@@ -33,9 +33,9 @@ class StoreDocumentationDocumentRequest extends FormRequest
'is_featured' => ['nullable', 'boolean'], 'is_featured' => ['nullable', 'boolean'],
'version' => ['required', 'string', 'max:50'], 'version' => ['required', 'string', 'max:50'],
'reference' => ['nullable', 'string', 'max:100'], 'reference' => ['nullable', 'string', 'max:100'],
'attachment' => ['nullable', 'file', 'max:' . config('documentation.max_upload_kb', 20480), 'mimes:' . implode(',', config('documentation.allowed_upload_mimes', []))], 'attachment' => ['nullable', 'file', 'max:'.config('documentation.max_upload_kb', 20480), 'mimes:'.implode(',', config('documentation.allowed_upload_mimes', []))],
'attachments' => ['nullable', 'array'], 'attachments' => ['nullable', 'array'],
'attachments.*' => ['file', 'max:' . config('documentation.max_upload_kb', 20480), 'mimes:' . implode(',', config('documentation.allowed_upload_mimes', []))], 'attachments.*' => ['file', 'max:'.config('documentation.max_upload_kb', 20480), 'mimes:'.implode(',', config('documentation.allowed_upload_mimes', []))],
]; ];
} }
} }
@@ -36,9 +36,9 @@ class UpdateDocumentationDocumentRequest extends FormRequest
'is_featured' => ['nullable', 'boolean'], 'is_featured' => ['nullable', 'boolean'],
'version' => ['required', 'string', 'max:50'], 'version' => ['required', 'string', 'max:50'],
'reference' => ['nullable', 'string', 'max:100'], 'reference' => ['nullable', 'string', 'max:100'],
'attachment' => ['nullable', 'file', 'max:' . config('documentation.max_upload_kb', 20480), 'mimes:' . implode(',', config('documentation.allowed_upload_mimes', []))], 'attachment' => ['nullable', 'file', 'max:'.config('documentation.max_upload_kb', 20480), 'mimes:'.implode(',', config('documentation.allowed_upload_mimes', []))],
'attachments' => ['nullable', 'array'], 'attachments' => ['nullable', 'array'],
'attachments.*' => ['file', 'max:' . config('documentation.max_upload_kb', 20480), 'mimes:' . implode(',', config('documentation.allowed_upload_mimes', []))], 'attachments.*' => ['file', 'max:'.config('documentation.max_upload_kb', 20480), 'mimes:'.implode(',', config('documentation.allowed_upload_mimes', []))],
'remove_attachment' => ['nullable', 'boolean'], 'remove_attachment' => ['nullable', 'boolean'],
]; ];
} }
@@ -57,13 +57,13 @@ class DocumentationAttachment extends Model
} }
if ($bytes < 1024) { if ($bytes < 1024) {
return $bytes . ' B'; return $bytes.' B';
} }
if ($bytes < 1048576) { if ($bytes < 1048576) {
return number_format($bytes / 1024, 1) . ' KB'; return number_format($bytes / 1024, 1).' KB';
} }
return number_format($bytes / 1048576, 1) . ' MB'; return number_format($bytes / 1048576, 1).' MB';
} }
} }
@@ -66,7 +66,7 @@ class DocumentationCategory extends Model
/** /**
* Active category scope. * Active category scope.
* *
* @param Builder<DocumentationCategory> $query * @param Builder<DocumentationCategory> $query
* @return Builder<DocumentationCategory> * @return Builder<DocumentationCategory>
*/ */
public function scopeActive(Builder $query): Builder public function scopeActive(Builder $query): Builder
@@ -77,7 +77,7 @@ class DocumentationCategory extends Model
/** /**
* Ordered category scope. * Ordered category scope.
* *
* @param Builder<DocumentationCategory> $query * @param Builder<DocumentationCategory> $query
* @return Builder<DocumentationCategory> * @return Builder<DocumentationCategory>
*/ */
public function scopeOrdered(Builder $query): Builder public function scopeOrdered(Builder $query): Builder
@@ -29,7 +29,9 @@ class DocumentationDocument extends Model
use SoftDeletes; use SoftDeletes;
public const STATUS_DRAFT = 'draft'; public const STATUS_DRAFT = 'draft';
public const STATUS_PUBLISHED = 'published'; public const STATUS_PUBLISHED = 'published';
public const STATUS_ARCHIVED = 'archived'; public const STATUS_ARCHIVED = 'archived';
/** @var list<string> */ /** @var list<string> */
@@ -99,7 +101,7 @@ class DocumentationDocument extends Model
/** /**
* Published document scope. * Published document scope.
* *
* @param Builder<DocumentationDocument> $query * @param Builder<DocumentationDocument> $query
* @return Builder<DocumentationDocument> * @return Builder<DocumentationDocument>
*/ */
public function scopePublished(Builder $query): Builder public function scopePublished(Builder $query): Builder
@@ -110,13 +112,14 @@ class DocumentationDocument extends Model
/** /**
* Featured document scope. * Featured document scope.
* *
* @param Builder<DocumentationDocument> $query * @param Builder<DocumentationDocument> $query
* @return Builder<DocumentationDocument> * @return Builder<DocumentationDocument>
*/ */
public function scopeFeatured(Builder $query): Builder public function scopeFeatured(Builder $query): Builder
{ {
return $query->where('is_featured', true); return $query->where('is_featured', true);
} }
/** /**
* Render the document body as safe Markdown HTML for display. * Render the document body as safe Markdown HTML for display.
*/ */
@@ -135,7 +138,7 @@ class DocumentationDocument extends Model
]); ]);
} }
return '<p>' . nl2br(e($body), false) . '</p>'; return '<p>'.nl2br(e($body), false).'</p>';
} }
/** /**
@@ -148,5 +151,4 @@ class DocumentationDocument extends Model
return str_ends_with($name, '.md') || in_array($mime, ['text/markdown', 'text/x-markdown', 'text/plain'], true); return str_ends_with($name, '.md') || in_array($mime, ['text/markdown', 'text/x-markdown', 'text/plain'], true);
} }
} }
@@ -2,8 +2,8 @@
namespace Modules\Documentation\Providers; namespace Modules\Documentation\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@@ -2,13 +2,13 @@
namespace Modules\Documentation\Tests\Feature; namespace Modules\Documentation\Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use Modules\Documentation\Models\DocumentationCategory; use Modules\Documentation\Models\DocumentationCategory;
use Modules\Documentation\Models\DocumentationDocument; use Modules\Documentation\Models\DocumentationDocument;
use Modules\Documentation\Models\DocumentationAttachment; use Tests\TestCase;
class DocumentationModuleTest extends TestCase class DocumentationModuleTest extends TestCase
{ {
@@ -17,7 +17,7 @@ class DocumentationModuleTest extends TestCase
/** /**
* Add a CSRF token for projects that keep web middleware enabled during tests. * Add a CSRF token for projects that keep web middleware enabled during tests.
* *
* @param array<string, mixed> $payload * @param array<string, mixed> $payload
* @return array<string, mixed> * @return array<string, mixed>
*/ */
private function csrfPayload(array $payload): array private function csrfPayload(array $payload): array
@@ -30,7 +30,7 @@ class DocumentationModuleTest extends TestCase
*/ */
public function test_documentation_dashboard_renders(): void public function test_documentation_dashboard_renders(): void
{ {
$user = \App\Models\User::factory()->create(); $user = User::factory()->create();
$this->actingAs($user) $this->actingAs($user)
->get(route('documentation.documents.index')) ->get(route('documentation.documents.index'))
@@ -43,7 +43,7 @@ class DocumentationModuleTest extends TestCase
*/ */
public function test_document_can_be_created(): void public function test_document_can_be_created(): void
{ {
$user = \App\Models\User::factory()->create(); $user = User::factory()->create();
$category = DocumentationCategory::query()->create([ $category = DocumentationCategory::query()->create([
'name' => 'Operations', 'name' => 'Operations',
'slug' => 'operations', 'slug' => 'operations',
@@ -72,7 +72,6 @@ class DocumentationModuleTest extends TestCase
]); ]);
} }
/** /**
* Markdown attachments are stored and imported into the document body. * Markdown attachments are stored and imported into the document body.
*/ */
@@ -80,7 +79,7 @@ class DocumentationModuleTest extends TestCase
{ {
Storage::fake('public'); Storage::fake('public');
$user = \App\Models\User::factory()->create(); $user = User::factory()->create();
$file = UploadedFile::fake()->createWithContent('goods-in.md', "# Goods In\n\n- Scan delivery note\n- Confirm quantities"); $file = UploadedFile::fake()->createWithContent('goods-in.md', "# Goods In\n\n- Scan delivery note\n- Confirm quantities");
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([ $response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
@@ -102,8 +101,6 @@ class DocumentationModuleTest extends TestCase
Storage::disk('public')->assertExists($document->attachment_path); Storage::disk('public')->assertExists($document->attachment_path);
} }
/** /**
* Multiple supporting attachments can be uploaded, downloaded and removed. * Multiple supporting attachments can be uploaded, downloaded and removed.
*/ */
@@ -111,7 +108,7 @@ class DocumentationModuleTest extends TestCase
{ {
Storage::fake('public'); Storage::fake('public');
$user = \App\Models\User::factory()->create(); $user = User::factory()->create();
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([ $response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
'title' => 'Attachment Pack', 'title' => 'Attachment Pack',
@@ -153,7 +150,7 @@ class DocumentationModuleTest extends TestCase
*/ */
public function test_document_editor_has_markdown_code_controls(): void public function test_document_editor_has_markdown_code_controls(): void
{ {
$user = \App\Models\User::factory()->create(); $user = User::factory()->create();
$this->actingAs($user) $this->actingAs($user)
->get(route('documentation.documents.create')) ->get(route('documentation.documents.create'))
@@ -168,7 +165,7 @@ class DocumentationModuleTest extends TestCase
*/ */
public function test_category_can_be_created(): void public function test_category_can_be_created(): void
{ {
$user = \App\Models\User::factory()->create(); $user = User::factory()->create();
$this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.categories.store'), $this->csrfPayload([ $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.categories.store'), $this->csrfPayload([
'name' => 'Policies', 'name' => 'Policies',
+1 -1
View File
@@ -7,7 +7,7 @@ return [
'middleware' => ['web', 'auth'], 'middleware' => ['web', 'auth'],
'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'local'), 'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'local'),
'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240), 'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240),
'allowed_mimes' => ['md','pdf', 'png', 'jpg', 'jpeg', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv', 'zip'], 'allowed_mimes' => ['md', 'pdf', 'png', 'jpg', 'jpeg', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv', 'zip'],
'default_status_slug' => 'new', 'default_status_slug' => 'new',
'closed_status_slug' => 'closed', 'closed_status_slug' => 'closed',
]; ];
@@ -24,7 +24,6 @@ return new class extends Migration
$table->unique(['company_id', 'slug']); $table->unique(['company_id', 'slug']);
}); });
Schema::create('helpdesk_sla_policies', static function (Blueprint $table): void { Schema::create('helpdesk_sla_policies', static function (Blueprint $table): void {
$table->id(); $table->id();
$table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete();
@@ -107,8 +106,6 @@ return new class extends Migration
$table->index(['category_id', 'team_id', 'is_active']); $table->index(['category_id', 'team_id', 'is_active']);
}); });
Schema::create('helpdesk_automation_rules', static function (Blueprint $table): void { Schema::create('helpdesk_automation_rules', static function (Blueprint $table): void {
$table->id(); $table->id();
$table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete();
@@ -126,7 +126,7 @@ final class TicketControlsController extends Controller
/** /**
* Apply a direct guarded ticket update without relying on Livewire hydration. * Apply a direct guarded ticket update without relying on Livewire hydration.
* *
* @param array<string, mixed> $updates * @param array<string, mixed> $updates
*/ */
private function updateTicket(Ticket $ticket, array $updates): void private function updateTicket(Ticket $ticket, array $updates): void
{ {
+1 -4
View File
@@ -6,8 +6,8 @@ namespace Modules\Helpdesk\Livewire;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
use Modules\Helpdesk\Models\KanbanBoard as KanbanBoardModel; use Modules\Helpdesk\Models\KanbanBoard as KanbanBoardModel;
use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Status;
@@ -99,9 +99,6 @@ final class KanbanBoard extends Component
/** /**
* Normalise the board selector value from Livewire/browser select events. * Normalise the board selector value from Livewire/browser select events.
*
* @param int|string|null $value
* @return void
*/ */
public function updatedBoardId(int|string|null $value): void public function updatedBoardId(int|string|null $value): void
{ {
+13 -17
View File
@@ -12,15 +12,15 @@ use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Modules\Helpdesk\Models\AutomationRule; use Modules\Helpdesk\Models\AutomationRule;
use Modules\Helpdesk\Models\CannedResponse; use Modules\Helpdesk\Models\CannedResponse;
use Modules\Helpdesk\Models\ChecklistTemplate;
use Modules\Helpdesk\Models\Category; use Modules\Helpdesk\Models\Category;
use Modules\Helpdesk\Models\ChecklistTemplate;
use Modules\Helpdesk\Models\GitRepository; use Modules\Helpdesk\Models\GitRepository;
use Modules\Helpdesk\Models\KanbanBoard; use Modules\Helpdesk\Models\KanbanBoard;
use Modules\Helpdesk\Models\Priority; use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Product; use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\Project; use Modules\Helpdesk\Models\Project;
use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\SlaPolicy; use Modules\Helpdesk\Models\SlaPolicy;
use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\Tag; use Modules\Helpdesk\Models\Tag;
use Modules\Helpdesk\Models\Team; use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Models\WorkflowTemplate; use Modules\Helpdesk\Models\WorkflowTemplate;
@@ -297,7 +297,6 @@ final class SettingsManager extends Component
$this->projectForm = ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null]; $this->projectForm = ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'is_default' => false, 'archived_at' => null];
} }
public function saveBoard(): void public function saveBoard(): void
{ {
$data = $this->validate([ $data = $this->validate([
@@ -435,7 +434,6 @@ final class SettingsManager extends Component
$this->workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true]; $this->workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
} }
public function saveCannedResponse(): void public function saveCannedResponse(): void
{ {
$data = $this->validate([ $data = $this->validate([
@@ -458,7 +456,7 @@ final class SettingsManager extends Component
public function editCannedResponse(int $id): void public function editCannedResponse(int $id): void
{ {
$this->cannedForm = CannedResponse::query()->findOrFail($id)->only(['id','company_id','category_id','team_id','name','slug','visibility','subject','body','is_active','sort_order']); $this->cannedForm = CannedResponse::query()->findOrFail($id)->only(['id', 'company_id', 'category_id', 'team_id', 'name', 'slug', 'visibility', 'subject', 'body', 'is_active', 'sort_order']);
$this->section = 'canned'; $this->section = 'canned';
} }
@@ -498,7 +496,7 @@ final class SettingsManager extends Component
public function editSlaPolicy(int $id): void public function editSlaPolicy(int $id): void
{ {
$this->slaForm = SlaPolicy::query()->findOrFail($id)->only(['id','company_id','priority_id','category_id','name','slug','first_response_minutes','resolution_minutes','business_hours_only','is_default','is_active']); $this->slaForm = SlaPolicy::query()->findOrFail($id)->only(['id', 'company_id', 'priority_id', 'category_id', 'name', 'slug', 'first_response_minutes', 'resolution_minutes', 'business_hours_only', 'is_default', 'is_active']);
$this->section = 'sla'; $this->section = 'sla';
} }
@@ -540,7 +538,7 @@ final class SettingsManager extends Component
public function editAutomationRule(int $id): void public function editAutomationRule(int $id): void
{ {
$this->automationForm = AutomationRule::query()->findOrFail($id)->only(['id','company_id','name','slug','match_sender','match_subject','match_mailbox','match_category_id','set_priority_id','set_category_id','set_team_id','set_project_id','set_status_id','set_agent_id','is_active','sort_order']); $this->automationForm = AutomationRule::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'match_sender', 'match_subject', 'match_mailbox', 'match_category_id', 'set_priority_id', 'set_category_id', 'set_team_id', 'set_project_id', 'set_status_id', 'set_agent_id', 'is_active', 'sort_order']);
$this->section = 'automation'; $this->section = 'automation';
} }
@@ -575,7 +573,7 @@ final class SettingsManager extends Component
public function editProduct(int $id): void public function editProduct(int $id): void
{ {
$this->productForm = Product::query()->findOrFail($id)->only(['id','company_id','name','slug','type','version','release_channel','description','is_active']); $this->productForm = Product::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'type', 'version', 'release_channel', 'description', 'is_active']);
$this->section = 'products'; $this->section = 'products';
} }
@@ -606,7 +604,7 @@ final class SettingsManager extends Component
public function editTag(int $id): void public function editTag(int $id): void
{ {
$this->tagForm = Tag::query()->findOrFail($id)->only(['id','company_id','name','slug','color']); $this->tagForm = Tag::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'color']);
$this->section = 'tags'; $this->section = 'tags';
} }
@@ -644,7 +642,7 @@ final class SettingsManager extends Component
public function editChecklistTemplate(int $id): void public function editChecklistTemplate(int $id): void
{ {
$template = ChecklistTemplate::query()->findOrFail($id); $template = ChecklistTemplate::query()->findOrFail($id);
$this->checklistTemplateForm = $template->only(['id','company_id','category_id','project_id','name','slug','is_active']) + [ $this->checklistTemplateForm = $template->only(['id', 'company_id', 'category_id', 'project_id', 'name', 'slug', 'is_active']) + [
'items_text' => collect($template->items ?? [])->map(fn ($item): string => is_array($item) ? (string) ($item['title'] ?? '') : (string) $item)->implode("\n"), 'items_text' => collect($template->items ?? [])->map(fn ($item): string => is_array($item) ? (string) ($item['title'] ?? '') : (string) $item)->implode("\n"),
]; ];
$this->section = 'checklists'; $this->section = 'checklists';
@@ -661,7 +659,6 @@ final class SettingsManager extends Component
$this->checklistTemplateForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true]; $this->checklistTemplateForm = ['id' => null, 'company_id' => null, 'category_id' => null, 'project_id' => null, 'name' => '', 'slug' => '', 'items_text' => '', 'is_active' => true];
} }
public function saveDefaultHelpdeskCompany(): void public function saveDefaultHelpdeskCompany(): void
{ {
if (! Schema::hasTable('companies') || ! Schema::hasColumn('companies', 'is_default_helpdesk_company')) { if (! Schema::hasTable('companies') || ! Schema::hasColumn('companies', 'is_default_helpdesk_company')) {
@@ -709,12 +706,14 @@ final class SettingsManager extends Component
'categories' => $this->orderedCategories(), 'categories' => $this->orderedCategories(),
'teams' => $this->orderedTeams(), 'teams' => $this->orderedTeams(),
'projects' => $this->orderedProjects(), 'projects' => $this->orderedProjects(),
'boardOwners' => User::query()->where(static function ($query): void { $query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true); })->orderBy('name')->get(['id', 'name', 'email']), 'boardOwners' => User::query()->where(static function ($query): void {
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
})->orderBy('name')->get(['id', 'name', 'email']),
'boards' => KanbanBoard::query()->with(['statuses', 'owner:id,name,email'])->withCount('tickets')->orderBy('sort_order')->orderBy('name')->get(), 'boards' => KanbanBoard::query()->with(['statuses', 'owner:id,name,email'])->withCount('tickets')->orderBy('sort_order')->orderBy('name')->get(),
'repositories' => GitRepository::query()->with(['project'])->withCount(['commits', 'webhookLogs'])->orderBy('name')->get(), 'repositories' => GitRepository::query()->with(['project'])->withCount(['commits', 'webhookLogs'])->orderBy('name')->get(),
'workflowTemplates' => WorkflowTemplate::query()->orderBy('event')->orderBy('name')->get(), 'workflowTemplates' => WorkflowTemplate::query()->orderBy('event')->orderBy('name')->get(),
'cannedResponses' => CannedResponse::query()->with(['category','team'])->orderBy('sort_order')->orderBy('name')->get(), 'cannedResponses' => CannedResponse::query()->with(['category', 'team'])->orderBy('sort_order')->orderBy('name')->get(),
'slaPolicies' => SlaPolicy::query()->with(['priority','category'])->orderByDesc('is_default')->orderBy('name')->get(), 'slaPolicies' => SlaPolicy::query()->with(['priority', 'category'])->orderByDesc('is_default')->orderBy('name')->get(),
'automationRules' => AutomationRule::query()->orderBy('sort_order')->orderBy('name')->get(), 'automationRules' => AutomationRule::query()->orderBy('sort_order')->orderBy('name')->get(),
'products' => Product::query()->withCount('tickets')->orderBy('name')->get(), 'products' => Product::query()->withCount('tickets')->orderBy('name')->get(),
'tags' => Tag::query()->orderBy('name')->get(), 'tags' => Tag::query()->orderBy('name')->get(),
@@ -722,8 +721,6 @@ final class SettingsManager extends Component
])->layout('layouts.app'); ])->layout('layouts.app');
} }
private function orderedStatuses(): mixed private function orderedStatuses(): mixed
{ {
return Status::query() return Status::query()
@@ -772,7 +769,6 @@ final class SettingsManager extends Component
->get(); ->get();
} }
/** /**
* Remove is_default before save when the database has not been migrated yet. * Remove is_default before save when the database has not been migrated yet.
* *
+1 -6
View File
@@ -17,8 +17,8 @@ use Modules\Helpdesk\Models\KanbanBoard;
use Modules\Helpdesk\Models\Priority; use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Product; use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\Project; use Modules\Helpdesk\Models\Project;
use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Models\Tag; use Modules\Helpdesk\Models\Tag;
use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Services\TicketService; use Modules\Helpdesk\Services\TicketService;
final class TicketCreate extends Component final class TicketCreate extends Component
@@ -55,7 +55,6 @@ final class TicketCreate extends Component
#[Validate('nullable|date')] #[Validate('nullable|date')]
public ?string $due_at = null; public ?string $due_at = null;
/** @var array<int, int> */ /** @var array<int, int> */
#[Validate('array')] #[Validate('array')]
public array $tag_ids = []; public array $tag_ids = [];
@@ -102,7 +101,6 @@ final class TicketCreate extends Component
return $this->redirectRoute('helpdesk.tickets.index', navigate: true); return $this->redirectRoute('helpdesk.tickets.index', navigate: true);
} }
/** /**
* Clear the create form, including Select2 and Flatpickr browser widgets. * Clear the create form, including Select2 and Flatpickr browser widgets.
*/ */
@@ -154,7 +152,6 @@ final class TicketCreate extends Component
->value('id'); ->value('id');
} }
if (blank($this->board_id) && Schema::hasTable('helpdesk_kanban_boards')) { if (blank($this->board_id) && Schema::hasTable('helpdesk_kanban_boards')) {
$this->board_id = KanbanBoard::query() $this->board_id = KanbanBoard::query()
->where('is_default', true) ->where('is_default', true)
@@ -213,8 +210,6 @@ final class TicketCreate extends Component
} }
} }
/** /**
* Return Kanban board options with the default board listed first. * Return Kanban board options with the default board listed first.
*/ */
-1
View File
@@ -7,7 +7,6 @@ namespace Modules\Helpdesk\Livewire;
use App\Models\User; use App\Models\User;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
+3 -2
View File
@@ -10,14 +10,15 @@ use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads; use Livewire\WithFileUploads;
use Modules\Helpdesk\Models\CannedResponse; use Modules\Helpdesk\Models\CannedResponse;
use Modules\Helpdesk\Models\ChecklistItem; use Modules\Helpdesk\Models\ChecklistItem;
use Modules\Helpdesk\Models\Comment;
use Modules\Helpdesk\Models\Priority; use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\Ticket; use Modules\Helpdesk\Models\Ticket;
@@ -299,7 +300,7 @@ final class TicketShow extends Component
$this->hydrateTicketUsers($ticket); $this->hydrateTicketUsers($ticket);
$comments = \Modules\Helpdesk\Models\Comment::query() $comments = Comment::query()
->where('ticket_id', $ticket->id) ->where('ticket_id', $ticket->id)
->with(['user.company', 'attachments']) ->with(['user.company', 'attachments'])
->oldest() ->oldest()
+4 -2
View File
@@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Model;
final class AutomationRule extends Model final class AutomationRule extends Model
{ {
protected $table = 'helpdesk_automation_rules'; protected $table = 'helpdesk_automation_rules';
protected $fillable = ['company_id','name','slug','match_sender','match_subject','match_mailbox','match_category_id','set_priority_id','set_category_id','set_team_id','set_project_id','set_status_id','set_agent_id','is_active','sort_order'];
protected $casts = ['company_id'=>'integer','match_category_id'=>'integer','set_priority_id'=>'integer','set_category_id'=>'integer','set_team_id'=>'integer','set_project_id'=>'integer','set_status_id'=>'integer','set_agent_id'=>'integer','is_active'=>'boolean','sort_order'=>'integer']; protected $fillable = ['company_id', 'name', 'slug', 'match_sender', 'match_subject', 'match_mailbox', 'match_category_id', 'set_priority_id', 'set_category_id', 'set_team_id', 'set_project_id', 'set_status_id', 'set_agent_id', 'is_active', 'sort_order'];
protected $casts = ['company_id' => 'integer', 'match_category_id' => 'integer', 'set_priority_id' => 'integer', 'set_category_id' => 'integer', 'set_team_id' => 'integer', 'set_project_id' => 'integer', 'set_status_id' => 'integer', 'set_agent_id' => 'integer', 'is_active' => 'boolean', 'sort_order' => 'integer'];
} }
+14 -4
View File
@@ -10,8 +10,18 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class CannedResponse extends Model final class CannedResponse extends Model
{ {
protected $table = 'helpdesk_canned_responses'; protected $table = 'helpdesk_canned_responses';
protected $fillable = ['company_id','category_id','team_id','name','slug','visibility','subject','body','is_active','sort_order'];
protected $casts = ['company_id'=>'integer','category_id'=>'integer','team_id'=>'integer','is_active'=>'boolean','sort_order'=>'integer']; protected $fillable = ['company_id', 'category_id', 'team_id', 'name', 'slug', 'visibility', 'subject', 'body', 'is_active', 'sort_order'];
public function category(): BelongsTo { return $this->belongsTo(Category::class, 'category_id'); }
public function team(): BelongsTo { return $this->belongsTo(Team::class, 'team_id'); } protected $casts = ['company_id' => 'integer', 'category_id' => 'integer', 'team_id' => 'integer', 'is_active' => 'boolean', 'sort_order' => 'integer'];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class, 'team_id');
}
} }
+9 -3
View File
@@ -10,7 +10,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class ChecklistItem extends Model final class ChecklistItem extends Model
{ {
protected $table = 'helpdesk_checklist_items'; protected $table = 'helpdesk_checklist_items';
protected $fillable = ['ticket_id','template_id','title','description','is_completed','completed_by','completed_at','sort_order'];
protected $casts = ['ticket_id'=>'integer','template_id'=>'integer','is_completed'=>'boolean','completed_by'=>'integer','completed_at'=>'datetime','sort_order'=>'integer']; protected $fillable = ['ticket_id', 'template_id', 'title', 'description', 'is_completed', 'completed_by', 'completed_at', 'sort_order'];
public function ticket(): BelongsTo { return $this->belongsTo(Ticket::class, 'ticket_id'); }
protected $casts = ['ticket_id' => 'integer', 'template_id' => 'integer', 'is_completed' => 'boolean', 'completed_by' => 'integer', 'completed_at' => 'datetime', 'sort_order' => 'integer'];
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
} }
@@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Model;
final class ChecklistTemplate extends Model final class ChecklistTemplate extends Model
{ {
protected $table = 'helpdesk_checklist_templates'; protected $table = 'helpdesk_checklist_templates';
protected $fillable = ['company_id','category_id','project_id','name','slug','items','is_active'];
protected $casts = ['company_id'=>'integer','category_id'=>'integer','project_id'=>'integer','items'=>'array','is_active'=>'boolean']; protected $fillable = ['company_id', 'category_id', 'project_id', 'name', 'slug', 'items', 'is_active'];
protected $casts = ['company_id' => 'integer', 'category_id' => 'integer', 'project_id' => 'integer', 'items' => 'array', 'is_active' => 'boolean'];
} }
+1 -1
View File
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Modules\Helpdesk\Models; namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Models\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
+9 -3
View File
@@ -10,7 +10,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
final class Product extends Model final class Product extends Model
{ {
protected $table = 'helpdesk_products'; protected $table = 'helpdesk_products';
protected $fillable = ['company_id','name','slug','type','version','release_channel','description','is_active'];
protected $casts = ['company_id'=>'integer','is_active'=>'boolean']; protected $fillable = ['company_id', 'name', 'slug', 'type', 'version', 'release_channel', 'description', 'is_active'];
public function tickets(): HasMany { return $this->hasMany(Ticket::class, 'product_id'); }
protected $casts = ['company_id' => 'integer', 'is_active' => 'boolean'];
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'product_id');
}
} }
+14 -4
View File
@@ -10,8 +10,18 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class SlaPolicy extends Model final class SlaPolicy extends Model
{ {
protected $table = 'helpdesk_sla_policies'; protected $table = 'helpdesk_sla_policies';
protected $fillable = ['company_id','priority_id','category_id','name','slug','first_response_minutes','resolution_minutes','business_hours_only','is_default','is_active'];
protected $casts = ['company_id'=>'integer','priority_id'=>'integer','category_id'=>'integer','first_response_minutes'=>'integer','resolution_minutes'=>'integer','business_hours_only'=>'boolean','is_default'=>'boolean','is_active'=>'boolean']; protected $fillable = ['company_id', 'priority_id', 'category_id', 'name', 'slug', 'first_response_minutes', 'resolution_minutes', 'business_hours_only', 'is_default', 'is_active'];
public function priority(): BelongsTo { return $this->belongsTo(Priority::class, 'priority_id'); }
public function category(): BelongsTo { return $this->belongsTo(Category::class, 'category_id'); } protected $casts = ['company_id' => 'integer', 'priority_id' => 'integer', 'category_id' => 'integer', 'first_response_minutes' => 'integer', 'resolution_minutes' => 'integer', 'business_hours_only' => 'boolean', 'is_default' => 'boolean', 'is_active' => 'boolean'];
public function priority(): BelongsTo
{
return $this->belongsTo(Priority::class, 'priority_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
} }
+9 -3
View File
@@ -10,7 +10,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
final class Tag extends Model final class Tag extends Model
{ {
protected $table = 'helpdesk_tags'; protected $table = 'helpdesk_tags';
protected $fillable = ['company_id','name','slug','color'];
protected $casts = ['company_id'=>'integer']; protected $fillable = ['company_id', 'name', 'slug', 'color'];
public function tickets(): BelongsToMany { return $this->belongsToMany(Ticket::class, 'helpdesk_ticket_tag', 'tag_id', 'ticket_id'); }
protected $casts = ['company_id' => 'integer'];
public function tickets(): BelongsToMany
{
return $this->belongsToMany(Ticket::class, 'helpdesk_ticket_tag', 'tag_id', 'ticket_id');
}
} }
-1
View File
@@ -114,7 +114,6 @@ final class Ticket extends Model
->oldest(); ->oldest();
} }
/** @return BelongsTo<SlaPolicy, Ticket> */ /** @return BelongsTo<SlaPolicy, Ticket> */
public function slaPolicy(): BelongsTo public function slaPolicy(): BelongsTo
{ {
+4 -2
View File
@@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Model;
final class TicketRating extends Model final class TicketRating extends Model
{ {
protected $table = 'helpdesk_ticket_ratings'; protected $table = 'helpdesk_ticket_ratings';
protected $fillable = ['ticket_id','agent_id','team_id','category_id','rating','token','comment','rated_at'];
protected $casts = ['ticket_id'=>'integer','agent_id'=>'integer','team_id'=>'integer','category_id'=>'integer','rating'=>'integer','rated_at'=>'datetime']; protected $fillable = ['ticket_id', 'agent_id', 'team_id', 'category_id', 'rating', 'token', 'comment', 'rated_at'];
protected $casts = ['ticket_id' => 'integer', 'agent_id' => 'integer', 'team_id' => 'integer', 'category_id' => 'integer', 'rating' => 'integer', 'rated_at' => 'datetime'];
} }
+4 -2
View File
@@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Model;
final class TicketSlaEvent extends Model final class TicketSlaEvent extends Model
{ {
protected $table = 'helpdesk_ticket_sla_events'; protected $table = 'helpdesk_ticket_sla_events';
protected $fillable = ['ticket_id','sla_policy_id','event','due_at','occurred_at','breached','meta'];
protected $casts = ['ticket_id'=>'integer','sla_policy_id'=>'integer','due_at'=>'datetime','occurred_at'=>'datetime','breached'=>'boolean','meta'=>'array']; protected $fillable = ['ticket_id', 'sla_policy_id', 'event', 'due_at', 'occurred_at', 'breached', 'meta'];
protected $casts = ['ticket_id' => 'integer', 'sla_policy_id' => 'integer', 'due_at' => 'datetime', 'occurred_at' => 'datetime', 'breached' => 'boolean', 'meta' => 'array'];
} }
+1 -1
View File
@@ -5,8 +5,8 @@ declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController; use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
use Modules\Helpdesk\Http\Controllers\HelpdeskController; use Modules\Helpdesk\Http\Controllers\HelpdeskController;
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
use Modules\Helpdesk\Http\Controllers\TicketControlsController; use Modules\Helpdesk\Http\Controllers\TicketControlsController;
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController; use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
use Modules\Helpdesk\Livewire\KanbanBoard; use Modules\Helpdesk\Livewire\KanbanBoard;
use Modules\Helpdesk\Livewire\SettingsManager; use Modules\Helpdesk\Livewire\SettingsManager;
@@ -9,7 +9,6 @@ use Modules\Helpdesk\Database\Seeders\HelpdeskDatabaseSeeder as DatabaseHelpdesk
final class HelpdeskDatabaseSeeder extends Seeder final class HelpdeskDatabaseSeeder extends Seeder
{ {
/** @return void */
public function run(): void public function run(): void
{ {
$this->call(DatabaseHelpdeskDatabaseSeeder::class); $this->call(DatabaseHelpdeskDatabaseSeeder::class);
@@ -66,5 +66,4 @@ final class CannedResponseRenderer
$ticket->setRelation('owner', $owner); $ticket->setRelation('owner', $owner);
$ticket->setRelation('agent', $agent); $ticket->setRelation('agent', $agent);
} }
} }
@@ -19,7 +19,7 @@ final class GitWebhookIngestionService
/** /**
* Persist supported Git provider commits and link them to matching Helpdesk ticket references. * Persist supported Git provider commits and link them to matching Helpdesk ticket references.
* *
* @param array<string, mixed> $payload * @param array<string, mixed> $payload
* @return array{status: string, message: string, commit_count: int, linked_count: int, ignored_count: int} * @return array{status: string, message: string, commit_count: int, linked_count: int, ignored_count: int}
*/ */
public function ingest(GitRepository $repository, array $payload, string $provider, ?string $deliveryId, ?string $event): array public function ingest(GitRepository $repository, array $payload, string $provider, ?string $deliveryId, ?string $event): array
@@ -102,8 +102,8 @@ final class GitWebhookIngestionService
/** /**
* Record a provider webhook acceptance, rejection or ignore decision for auditability. * Record a provider webhook acceptance, rejection or ignore decision for auditability.
* *
* @param array{status: string, message: string, commit_count?: int, linked_count?: int, ignored_count?: int} $result * @param array{status: string, message: string, commit_count?: int, linked_count?: int, ignored_count?: int} $result
* @param array<string, mixed>|null $payload * @param array<string, mixed>|null $payload
*/ */
public function logWebhook(GitRepository $repository, string $provider, ?string $deliveryId, ?string $event, array $result, ?array $payload = null): void public function logWebhook(GitRepository $repository, string $provider, ?string $deliveryId, ?string $event, array $result, ?array $payload = null): void
{ {
@@ -158,9 +158,6 @@ final class GitWebhookIngestionService
}; };
} }
/**
* @param mixed $value
*/
private function branchFromRef(mixed $value): ?string private function branchFromRef(mixed $value): ?string
{ {
$ref = $this->stringOrNull($value); $ref = $this->stringOrNull($value);
@@ -173,7 +170,7 @@ final class GitWebhookIngestionService
} }
/** /**
* @param array<string, mixed> $payload * @param array<string, mixed> $payload
* @return array<int, array<string, mixed>> * @return array<int, array<string, mixed>>
*/ */
private function commitsFromPayload(array $payload): array private function commitsFromPayload(array $payload): array
@@ -208,9 +205,6 @@ final class GitWebhookIngestionService
return $query->first(); return $query->first();
} }
/**
* @param mixed $value
*/
private function parseTimestamp(mixed $value): ?CarbonImmutable private function parseTimestamp(mixed $value): ?CarbonImmutable
{ {
$timestamp = $this->stringOrNull($value); $timestamp = $this->stringOrNull($value);
@@ -226,9 +220,6 @@ final class GitWebhookIngestionService
} }
} }
/**
* @param mixed $value
*/
private function stringOrNull(mixed $value): ?string private function stringOrNull(mixed $value): ?string
{ {
if (! is_scalar($value)) { if (! is_scalar($value)) {
@@ -9,10 +9,10 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Modules\Helpdesk\Models\Ticket; use Modules\Helpdesk\Models\Ticket;
use Modules\Helpdesk\Models\WorkflowLog; use Modules\Helpdesk\Models\WorkflowLog;
use Modules\Helpdesk\Models\WorkflowTemplate; use Modules\Helpdesk\Models\WorkflowTemplate;
use Illuminate\Support\Str;
use Throwable; use Throwable;
final class WorkflowNotificationService final class WorkflowNotificationService
@@ -88,6 +88,7 @@ if (! component) {
window.Livewire.find(component.getAttribute('wire:id')).set({$encodedProperty}, {$encodedValue}); window.Livewire.find(component.getAttribute('wire:id')).set({$encodedProperty}, {$encodedValue});
JS); JS);
} }
private function setTicketShowProperty(Browser $browser, string $property, mixed $value): void private function setTicketShowProperty(Browser $browser, string $property, mixed $value): void
{ {
$encodedProperty = json_encode($property, JSON_THROW_ON_ERROR); $encodedProperty = json_encode($property, JSON_THROW_ON_ERROR);
@@ -95,6 +95,7 @@ final class KanbanBoardTest extends TestCase
$this->assertSame($targetBoard->id, $ticket->board_id); $this->assertSame($targetBoard->id, $ticket->board_id);
} }
/** @test */ /** @test */
public function kanban_board_filters_tickets_to_the_selected_board_only(): void public function kanban_board_filters_tickets_to_the_selected_board_only(): void
{ {
@@ -135,5 +136,4 @@ final class KanbanBoardTest extends TestCase
->assertSee('Escalation board ticket') ->assertSee('Escalation board ticket')
->assertDontSee('Default board ticket'); ->assertDontSee('Default board ticket');
} }
} }
@@ -13,9 +13,7 @@ use Modules\Helpdesk\Models\ChecklistItem;
use Modules\Helpdesk\Models\ChecklistTemplate; use Modules\Helpdesk\Models\ChecklistTemplate;
use Modules\Helpdesk\Models\Product; use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\SlaPolicy; use Modules\Helpdesk\Models\SlaPolicy;
use Modules\Helpdesk\Models\Ticket;
use Modules\Helpdesk\Models\TicketSlaEvent; use Modules\Helpdesk\Models\TicketSlaEvent;
use Modules\Helpdesk\Services\AutomationRuleService;
use Modules\Helpdesk\Services\ChecklistService; use Modules\Helpdesk\Services\ChecklistService;
use Modules\Helpdesk\Services\SlaService; use Modules\Helpdesk\Services\SlaService;
use Modules\Helpdesk\Services\TicketService; use Modules\Helpdesk\Services\TicketService;
+1 -1
View File
@@ -5,8 +5,8 @@ declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController; use Modules\Helpdesk\Http\Controllers\AttachmentDownloadController;
use Modules\Helpdesk\Http\Controllers\HelpdeskController; use Modules\Helpdesk\Http\Controllers\HelpdeskController;
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
use Modules\Helpdesk\Http\Controllers\TicketControlsController; use Modules\Helpdesk\Http\Controllers\TicketControlsController;
use Modules\Helpdesk\Http\Controllers\TicketStoreController;
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController; use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
use Modules\Helpdesk\Livewire\KanbanBoard; use Modules\Helpdesk\Livewire\KanbanBoard;
use Modules\Helpdesk\Livewire\SettingsManager; use Modules\Helpdesk\Livewire\SettingsManager;
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateActivityLogTable extends Migration class CreateActivityLogTable extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddEventColumnToActivityLogTable extends Migration class AddEventColumnToActivityLogTable extends Migration
{ {
@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddBatchUuidColumnToActivityLogTable extends Migration class AddBatchUuidColumnToActivityLogTable extends Migration
{ {
+4 -3
View File
@@ -1,5 +1,6 @@
<?php <?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::view('/', 'welcome')->name('home'); Route::view('/', 'welcome')->name('home');
@@ -14,7 +15,7 @@ require __DIR__.'/settings.php';
Route::get('/api/countries/resolve/{code}', function (string $code) { Route::get('/api/countries/resolve/{code}', function (string $code) {
$normalised = strtoupper(trim($code)); $normalised = strtoupper(trim($code));
$country = \Illuminate\Support\Facades\DB::table('countries') $country = DB::table('countries')
->whereRaw('UPPER(alpha2) = ?', [$normalised]) ->whereRaw('UPPER(alpha2) = ?', [$normalised])
->orWhereRaw('UPPER(alpha3) = ?', [$normalised]) ->orWhereRaw('UPPER(alpha3) = ?', [$normalised])
->orWhere('numeric', $normalised) ->orWhere('numeric', $normalised)
@@ -43,7 +44,7 @@ Route::get('/api/countries/resolve/{code}', function (string $code) {
Route::get('/api/countries/{code}/subdivisions', function (string $code) { Route::get('/api/countries/{code}/subdivisions', function (string $code) {
$normalised = strtoupper(trim($code)); $normalised = strtoupper(trim($code));
$country = \Illuminate\Support\Facades\DB::table('countries') $country = DB::table('countries')
->whereRaw('UPPER(alpha2) = ?', [$normalised]) ->whereRaw('UPPER(alpha2) = ?', [$normalised])
->orWhereRaw('UPPER(alpha3) = ?', [$normalised]) ->orWhereRaw('UPPER(alpha3) = ?', [$normalised])
->orWhere('numeric', $normalised) ->orWhere('numeric', $normalised)
@@ -51,7 +52,7 @@ Route::get('/api/countries/{code}/subdivisions', function (string $code) {
abort_if($country === null, 404); abort_if($country === null, 404);
$subdivisions = \Illuminate\Support\Facades\DB::table('country_subdivisions') $subdivisions = DB::table('country_subdivisions')
->where('country_id', $country->id) ->where('country_id', $country->id)
->orderBy('sort_order') ->orderBy('sort_order')
->orderBy('name') ->orderBy('name')
+2 -3
View File
@@ -30,7 +30,7 @@ abstract class DuskTestCase extends BaseTestCase
*/ */
protected function driver(): RemoteWebDriver protected function driver(): RemoteWebDriver
{ {
$options = (new ChromeOptions())->addArguments([ $options = (new ChromeOptions)->addArguments([
'--disable-gpu', '--disable-gpu',
'--headless=new', '--headless=new',
'--window-size=1920,1080', '--window-size=1920,1080',
@@ -80,8 +80,7 @@ abstract class DuskTestCase extends BaseTestCase
)); ));
usort($candidates, static function (string $left, string $right): int { usort($candidates, static function (string $left, string $right): int {
$score = static fn (string $path): int => $score = static fn (string $path): int => str_contains($path, 'mac-arm64') ? 0
str_contains($path, 'mac-arm64') ? 0
: (str_contains($path, 'mac-arm') ? 1 : (str_contains($path, 'mac-arm') ? 1
: (str_contains($path, 'mac') ? 2 : 3)); : (str_contains($path, 'mac') ? 2 : 3));