Compare commits
18
Commits
449fb0bf91
...
856038c762
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
856038c762 | ||
|
|
5d58e35275 | ||
|
|
16fe344eef | ||
|
|
4375fc729f | ||
|
|
37a116437e | ||
|
|
56b5cb91b0 | ||
|
|
ce79962289 | ||
|
|
5aeeb87b21 | ||
|
|
744968120a | ||
|
|
8c0b214a9d | ||
|
|
279d2761e3 | ||
|
|
20bdb82ef9 | ||
|
|
66e38cfe52 | ||
|
|
8105d1cb6a | ||
|
|
84a628277d | ||
|
|
45a370ed91 | ||
|
|
0756b1de79 | ||
|
|
cf2b61f964 |
+1
-1
@@ -26,7 +26,6 @@ yarn-error.log
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
package-lock.json
|
||||
composer.lock
|
||||
/provision/
|
||||
/storage/logs/
|
||||
@@ -39,3 +38,4 @@ composer.lock
|
||||
/storage/framework/
|
||||
/storage/debugbar/
|
||||
/phpunit.dusk.xml
|
||||
/sonarqube-issues.csv
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>When</th>
|
||||
|
||||
@@ -63,9 +63,7 @@ class AuditTrailServiceProvider extends ServiceProvider
|
||||
$commandPath = __DIR__ . '/../Console';
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(function ($file) {
|
||||
$class = 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
|
||||
return $class;
|
||||
return 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
})
|
||||
->filter(function ($class) {
|
||||
return class_exists($class);
|
||||
|
||||
@@ -14,17 +14,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $moduleNamespace = 'Modules\AuditTrail\Http\Controllers';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Carrier</th>
|
||||
|
||||
@@ -26,40 +26,37 @@ class DeleteOpenAiFilesCommand extends Command
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$service = new OpenAiFileService((string) $setting->api_key);
|
||||
|
||||
try {
|
||||
if ((bool) $this->option('dry-run')) {
|
||||
$files = $service->listFiles();
|
||||
|
||||
$this->info('Found ' . count($files) . ' OpenAI API file(s).');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->line(sprintf(
|
||||
'- %s %s',
|
||||
(string) ($file['id'] ?? 'unknown'),
|
||||
(string) ($file['filename'] ?? 'unnamed'),
|
||||
));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$result = $service->deleteAllFiles();
|
||||
|
||||
$this->info("Listed: {$result['listed']}");
|
||||
$this->info("Deleted: {$result['deleted']}");
|
||||
$this->info("Failed: {$result['failed']}");
|
||||
|
||||
foreach ($result['failures'] as $failure) {
|
||||
$this->error($failure['file_id'] . ': ' . $failure['error']);
|
||||
}
|
||||
|
||||
return $result['failed'] === 0 ? self::SUCCESS : self::FAILURE;
|
||||
return $this->executeFor(new OpenAiFileService((string) $setting->api_key));
|
||||
} catch (Throwable $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function executeFor(OpenAiFileService $service): int
|
||||
{
|
||||
if ((bool) $this->option('dry-run')) {
|
||||
$files = $service->listFiles();
|
||||
$this->info('Found ' . count($files) . ' OpenAI API file(s).');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$this->line(sprintf('- %s %s', (string) ($file['id'] ?? 'unknown'), (string) ($file['filename'] ?? 'unnamed')));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$result = $service->deleteAllFiles();
|
||||
$this->info("Listed: {$result['listed']}");
|
||||
$this->info("Deleted: {$result['deleted']}");
|
||||
$this->info("Failed: {$result['failed']}");
|
||||
|
||||
foreach ($result['failures'] as $failure) {
|
||||
$this->error($failure['file_id'] . ': ' . $failure['error']);
|
||||
}
|
||||
|
||||
return $result['failed'] === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@ class ChatGPTDatabaseSeeder extends Seeder
|
||||
{
|
||||
Model::unguard();
|
||||
|
||||
// $this->call("OthersTableSeeder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\ChatGPT\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class OpenAiFileException extends RuntimeException {}
|
||||
@@ -79,9 +79,7 @@ class ChatGPTServiceProvider extends ServiceProvider
|
||||
$commandPath = __DIR__ . '/../Console';
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(function ($file) {
|
||||
$class = 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
|
||||
return $class;
|
||||
return 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
})
|
||||
->filter(function ($class) {
|
||||
return class_exists($class);
|
||||
|
||||
@@ -14,17 +14,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $moduleNamespace = 'Modules\ChatGPT\Http\Controllers';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
|
||||
<div class="mt-5 overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Name</th>
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Modules\ChatGPT\Services;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
use Modules\ChatGPT\Exceptions\OpenAiFileException;
|
||||
|
||||
class OpenAiFileService
|
||||
{
|
||||
@@ -33,7 +33,7 @@ class OpenAiFileService
|
||||
->get(self::BASE_URL . '/files', $query);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException('OpenAI file list request failed: ' . $response->body());
|
||||
throw new OpenAiFileException('OpenAI file list request failed: ' . $response->body());
|
||||
}
|
||||
|
||||
$payload = $response->json();
|
||||
@@ -63,7 +63,7 @@ class OpenAiFileService
|
||||
->delete(self::BASE_URL . '/files/' . rawurlencode($fileId));
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException("OpenAI file delete request failed for {$fileId}: " . $response->body());
|
||||
throw new OpenAiFileException("OpenAI file delete request failed for {$fileId}: " . $response->body());
|
||||
}
|
||||
|
||||
return $response->json() ?? [];
|
||||
@@ -90,7 +90,7 @@ class OpenAiFileService
|
||||
try {
|
||||
$this->deleteFile($fileId);
|
||||
$deleted++;
|
||||
} catch (RuntimeException $exception) {
|
||||
} catch (OpenAiFileException $exception) {
|
||||
$failures[] = [
|
||||
'file_id' => $fileId,
|
||||
'error' => $exception->getMessage(),
|
||||
|
||||
@@ -11,6 +11,7 @@ use Modules\Warehouse\app\Models\Warehouse;
|
||||
|
||||
class CompanyAddressController extends Controller
|
||||
{
|
||||
private const MAX_STRING = self::MAX_STRING;
|
||||
public function updateCompany(Request $request, Company $company)
|
||||
{
|
||||
$this->upsertAddress($request, $company);
|
||||
@@ -40,12 +41,12 @@ class CompanyAddressController extends Controller
|
||||
{
|
||||
$data = $request->validate([
|
||||
'label' => ['nullable', 'string', 'max:80'],
|
||||
'attention' => ['nullable', 'string', 'max:255'],
|
||||
'line_1' => ['required', 'string', 'max:255'],
|
||||
'line_2' => ['nullable', 'string', 'max:255'],
|
||||
'line_3' => ['nullable', 'string', 'max:255'],
|
||||
'locality' => ['required', 'string', 'max:255'],
|
||||
'administrative_area' => ['nullable', 'string', 'max:255'],
|
||||
'attention' => ['nullable', 'string', self::MAX_STRING],
|
||||
'line_1' => ['required', 'string', self::MAX_STRING],
|
||||
'line_2' => ['nullable', 'string', self::MAX_STRING],
|
||||
'line_3' => ['nullable', 'string', self::MAX_STRING],
|
||||
'locality' => ['required', 'string', self::MAX_STRING],
|
||||
'administrative_area' => ['nullable', 'string', self::MAX_STRING],
|
||||
'postal_code' => ['nullable', 'string', 'max:32'],
|
||||
'country_id' => ['required', Rule::exists('countries', 'id')->where('is_active', true)],
|
||||
'country_subdivision_id' => ['nullable', 'exists:country_subdivisions,id'],
|
||||
|
||||
@@ -45,7 +45,7 @@ class CompanyController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$company = Company::create($this->validated($request));
|
||||
Company::create($this->validated($request));
|
||||
|
||||
return redirect()
|
||||
->route('platform.companies.index')
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
|
||||
<div class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<table class="min-w-full text-sm">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead>
|
||||
<tr class="border-b border-zinc-200 dark:border-zinc-800">
|
||||
<th class="p-4 text-left">Name</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Setting</th>
|
||||
|
||||
@@ -8,6 +8,11 @@ use Modules\Countries\Support\Iso3166;
|
||||
|
||||
class StoreCountryRequest extends FormRequest
|
||||
{
|
||||
private const ISO3 = 'size:3';
|
||||
|
||||
private const MAX_STRING = 'max:255';
|
||||
|
||||
private const MAX_SHORT = 'max:16';
|
||||
/**
|
||||
* Authorize country reference-data administration.
|
||||
*/
|
||||
@@ -38,29 +43,29 @@ class StoreCountryRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'alpha2' => ['required', 'string', 'size:2', 'uppercase', 'unique:countries,alpha2'],
|
||||
'alpha3' => ['required', 'string', 'size:3', 'uppercase', 'unique:countries,alpha3'],
|
||||
'numeric' => ['required', 'string', 'size:3', 'unique:countries,numeric'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'official_name' => ['nullable', 'string', 'max:255'],
|
||||
'common_name' => ['nullable', 'string', 'max:255'],
|
||||
'display_name' => ['nullable', 'string', 'max:255'],
|
||||
'native_name' => ['nullable', 'string', 'max:255'],
|
||||
'capital' => ['nullable', 'string', 'max:255'],
|
||||
'flag_emoji' => ['nullable', 'string', 'max:16'],
|
||||
'calling_code' => ['nullable', 'string', 'max:16'],
|
||||
'tld' => ['nullable', 'string', 'max:16'],
|
||||
'currency_code' => ['nullable', 'string', 'size:3', 'uppercase'],
|
||||
'currency_name' => ['nullable', 'string', 'max:255'],
|
||||
'currency_symbol' => ['nullable', 'string', 'max:16'],
|
||||
'alpha3' => ['required', 'string', self::ISO3, 'uppercase', 'unique:countries,alpha3'],
|
||||
'numeric' => ['required', 'string', self::ISO3, 'unique:countries,numeric'],
|
||||
'name' => ['required', 'string', self::MAX_STRING],
|
||||
'official_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'common_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'display_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'native_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'capital' => ['nullable', 'string', self::MAX_STRING],
|
||||
'flag_emoji' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'calling_code' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'tld' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'currency_code' => ['nullable', 'string', self::ISO3, 'uppercase'],
|
||||
'currency_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'currency_symbol' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'languages' => ['nullable', 'json'],
|
||||
'timezones' => ['nullable', 'json'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'flag_svg_url' => ['nullable', 'url', 'max:255'],
|
||||
'flag_png_url' => ['nullable', 'url', 'max:255'],
|
||||
'flag_svg_url' => ['nullable', 'url', self::MAX_STRING],
|
||||
'flag_png_url' => ['nullable', 'url', self::MAX_STRING],
|
||||
'ioc_code' => ['nullable', 'string', 'max:8'],
|
||||
'fifa_code' => ['nullable', 'string', 'max:8'],
|
||||
'postal_code_format' => ['nullable', 'string', 'max:255'],
|
||||
'postal_code_format' => ['nullable', 'string', self::MAX_STRING],
|
||||
'address_format' => ['nullable', 'string', 'max:5000'],
|
||||
'is_active' => ['required', 'boolean'],
|
||||
'iso_status' => ['required', 'string', Rule::in(Iso3166::statuses())],
|
||||
@@ -70,10 +75,10 @@ class StoreCountryRequest extends FormRequest
|
||||
'continent' => ['nullable', 'string', 'max:64'],
|
||||
'region' => ['nullable', 'string', 'max:100'],
|
||||
'subregion' => ['nullable', 'string', 'max:100'],
|
||||
'intermediate_region' => ['nullable', 'string', 'max:255'],
|
||||
'intermediate_region' => ['nullable', 'string', self::MAX_STRING],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0', 'max:999999'],
|
||||
'source' => ['nullable', 'string', 'max:255'],
|
||||
'source_version' => ['nullable', 'string', 'max:255'],
|
||||
'source' => ['nullable', 'string', self::MAX_STRING],
|
||||
'source_version' => ['nullable', 'string', self::MAX_STRING],
|
||||
'last_synced_at' => ['nullable', 'date'],
|
||||
'metadata' => ['nullable', 'json'],
|
||||
];
|
||||
|
||||
@@ -8,6 +8,11 @@ use Modules\Countries\Support\Iso3166;
|
||||
|
||||
class UpdateCountryRequest extends FormRequest
|
||||
{
|
||||
private const ISO3 = 'size:3';
|
||||
|
||||
private const MAX_STRING = 'max:255';
|
||||
|
||||
private const MAX_SHORT = 'max:16';
|
||||
/**
|
||||
* Authorize country reference-data administration.
|
||||
*/
|
||||
@@ -40,29 +45,29 @@ class UpdateCountryRequest extends FormRequest
|
||||
|
||||
return [
|
||||
'alpha2' => ['required', 'string', 'size:2', 'uppercase', Rule::unique('countries', 'alpha2')->ignore($countryId)],
|
||||
'alpha3' => ['required', 'string', 'size:3', 'uppercase', Rule::unique('countries', 'alpha3')->ignore($countryId)],
|
||||
'numeric' => ['required', 'string', 'size:3', Rule::unique('countries', 'numeric')->ignore($countryId)],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'official_name' => ['nullable', 'string', 'max:255'],
|
||||
'common_name' => ['nullable', 'string', 'max:255'],
|
||||
'display_name' => ['nullable', 'string', 'max:255'],
|
||||
'native_name' => ['nullable', 'string', 'max:255'],
|
||||
'capital' => ['nullable', 'string', 'max:255'],
|
||||
'flag_emoji' => ['nullable', 'string', 'max:16'],
|
||||
'calling_code' => ['nullable', 'string', 'max:16'],
|
||||
'tld' => ['nullable', 'string', 'max:16'],
|
||||
'currency_code' => ['nullable', 'string', 'size:3', 'uppercase'],
|
||||
'currency_name' => ['nullable', 'string', 'max:255'],
|
||||
'currency_symbol' => ['nullable', 'string', 'max:16'],
|
||||
'alpha3' => ['required', 'string', self::ISO3, 'uppercase', Rule::unique('countries', 'alpha3')->ignore($countryId)],
|
||||
'numeric' => ['required', 'string', self::ISO3, Rule::unique('countries', 'numeric')->ignore($countryId)],
|
||||
'name' => ['required', 'string', self::MAX_STRING],
|
||||
'official_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'common_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'display_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'native_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'capital' => ['nullable', 'string', self::MAX_STRING],
|
||||
'flag_emoji' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'calling_code' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'tld' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'currency_code' => ['nullable', 'string', self::ISO3, 'uppercase'],
|
||||
'currency_name' => ['nullable', 'string', self::MAX_STRING],
|
||||
'currency_symbol' => ['nullable', 'string', self::MAX_SHORT],
|
||||
'languages' => ['nullable', 'json'],
|
||||
'timezones' => ['nullable', 'json'],
|
||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
|
||||
'flag_svg_url' => ['nullable', 'url', 'max:255'],
|
||||
'flag_png_url' => ['nullable', 'url', 'max:255'],
|
||||
'flag_svg_url' => ['nullable', 'url', self::MAX_STRING],
|
||||
'flag_png_url' => ['nullable', 'url', self::MAX_STRING],
|
||||
'ioc_code' => ['nullable', 'string', 'max:8'],
|
||||
'fifa_code' => ['nullable', 'string', 'max:8'],
|
||||
'postal_code_format' => ['nullable', 'string', 'max:255'],
|
||||
'postal_code_format' => ['nullable', 'string', self::MAX_STRING],
|
||||
'address_format' => ['nullable', 'string', 'max:5000'],
|
||||
'is_active' => ['required', 'boolean'],
|
||||
'iso_status' => ['required', 'string', Rule::in(Iso3166::statuses())],
|
||||
@@ -72,10 +77,10 @@ class UpdateCountryRequest extends FormRequest
|
||||
'continent' => ['nullable', 'string', 'max:64'],
|
||||
'region' => ['nullable', 'string', 'max:100'],
|
||||
'subregion' => ['nullable', 'string', 'max:100'],
|
||||
'intermediate_region' => ['nullable', 'string', 'max:255'],
|
||||
'intermediate_region' => ['nullable', 'string', self::MAX_STRING],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0', 'max:999999'],
|
||||
'source' => ['nullable', 'string', 'max:255'],
|
||||
'source_version' => ['nullable', 'string', 'max:255'],
|
||||
'source' => ['nullable', 'string', self::MAX_STRING],
|
||||
'source_version' => ['nullable', 'string', self::MAX_STRING],
|
||||
'last_synced_at' => ['nullable', 'date'],
|
||||
'metadata' => ['nullable', 'json'],
|
||||
];
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-950">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-950">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">Area
|
||||
@@ -159,6 +160,7 @@
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-950">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold uppercase tracking-wide text-zinc-500">Code
|
||||
|
||||
@@ -5,6 +5,8 @@ use Modules\Countries\Http\Controllers\CountriesController;
|
||||
use Modules\Countries\Http\Controllers\CountrySyncController;
|
||||
use Modules\Countries\Http\Controllers\ReferenceDataController;
|
||||
|
||||
const COUNTRY_ROUTE = COUNTRY_ROUTE;
|
||||
|
||||
Route::prefix('countries')->name('countries.')->middleware(['web', 'auth'])->group(function (): void {
|
||||
Route::get('/', CountriesController::class)->name('index');
|
||||
Route::get('/create', [CountriesController::class, 'create'])->name('create');
|
||||
@@ -15,8 +17,8 @@ Route::prefix('countries')->name('countries.')->middleware(['web', 'auth'])->gro
|
||||
Route::get('/reference/languages', [ReferenceDataController::class, 'languages'])->name('languages.index');
|
||||
Route::get('/reference/scripts', [ReferenceDataController::class, 'scripts'])->name('scripts.index');
|
||||
Route::get('/reference/locodes', [ReferenceDataController::class, 'locodes'])->name('locodes.index');
|
||||
Route::get('/{country:alpha2}', [CountriesController::class, 'show'])->name('show');
|
||||
Route::get('/{country:alpha2}/edit', [CountriesController::class, 'edit'])->name('edit');
|
||||
Route::put('/{country:alpha2}', [CountriesController::class, 'update'])->name('update');
|
||||
Route::delete('/{country:alpha2}', [CountriesController::class, 'destroy'])->name('destroy');
|
||||
Route::get(COUNTRY_ROUTE, [CountriesController::class, 'show'])->name('show');
|
||||
Route::get(COUNTRY_ROUTE . '/edit', [CountriesController::class, 'edit'])->name('edit');
|
||||
Route::put(COUNTRY_ROUTE, [CountriesController::class, 'update'])->name('update');
|
||||
Route::delete(COUNTRY_ROUTE, [CountriesController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ final class Iso3166
|
||||
{
|
||||
$normalised = str_pad((string) $code, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
if (! preg_match('/^[0-9]{3}$/', $normalised)) {
|
||||
if (! preg_match('/^\d{3}$/', $normalised)) {
|
||||
throw new InvalidArgumentException('ISO 3166-1 numeric codes must contain exactly three digits.');
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
|
||||
@@ -15,6 +15,9 @@ use Modules\Documentation\Models\DocumentationDocument;
|
||||
|
||||
class DocumentationController extends Controller
|
||||
{
|
||||
private const string STORAGE_DISK_CONFIG = 'documentation.storage_disk';
|
||||
|
||||
private const string UPLOAD_PATH_CONFIG = 'documentation.upload_path';
|
||||
/**
|
||||
* Display the documentation dashboard and document list.
|
||||
*/
|
||||
@@ -170,7 +173,7 @@ class DocumentationController extends Controller
|
||||
{
|
||||
abort_if(blank($document->attachment_path), 404);
|
||||
|
||||
return Storage::disk($document->attachment_disk ?: config('documentation.storage_disk', 'public'))
|
||||
return Storage::disk($document->attachment_disk ?: config(self::STORAGE_DISK_CONFIG, 'public'))
|
||||
->download($document->attachment_path, $document->attachment_name);
|
||||
}
|
||||
|
||||
@@ -202,27 +205,19 @@ class DocumentationController extends Controller
|
||||
*/
|
||||
private function extractMarkdownBody(Request $request): ?string
|
||||
{
|
||||
if (! config('documentation.import_markdown_attachments', true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = $request->file('attachment');
|
||||
$canImport = config('documentation.import_markdown_attachments', true)
|
||||
&& $file !== null
|
||||
&& $this->isMarkdownUpload($request)
|
||||
&& $file->getSize() <= (int) config('documentation.max_markdown_import_bytes', 1048576);
|
||||
|
||||
if ($file === null || ! $this->isMarkdownUpload($request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($file->getSize() > (int) config('documentation.max_markdown_import_bytes', 1048576)) {
|
||||
if (! $canImport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getRealPath());
|
||||
|
||||
if ($content === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim($content);
|
||||
return $content === false ? null : trim($content);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,8 +245,8 @@ class DocumentationController extends Controller
|
||||
private function storeAttachment(Request $request): array
|
||||
{
|
||||
$file = $request->file('attachment');
|
||||
$disk = config('documentation.storage_disk', 'public');
|
||||
$path = $file->store(config('documentation.upload_path', 'documentation'), $disk);
|
||||
$disk = config(self::STORAGE_DISK_CONFIG, 'public');
|
||||
$path = $file->store(config(self::UPLOAD_PATH_CONFIG, 'documentation'), $disk);
|
||||
|
||||
return [
|
||||
'attachment_disk' => $disk,
|
||||
@@ -271,14 +266,14 @@ class DocumentationController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$disk = config('documentation.storage_disk', 'public');
|
||||
$disk = config(self::STORAGE_DISK_CONFIG, 'public');
|
||||
|
||||
foreach ((array) $request->file('attachments') as $file) {
|
||||
if ($file === null || ! $file->isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $file->store(config('documentation.upload_path', 'documentation'), $disk);
|
||||
$path = $file->store(config(self::UPLOAD_PATH_CONFIG, 'documentation'), $disk);
|
||||
|
||||
$document->attachments()->create([
|
||||
'disk' => $disk,
|
||||
@@ -297,7 +292,7 @@ class DocumentationController extends Controller
|
||||
private function deleteAttachment(DocumentationDocument $document): void
|
||||
{
|
||||
if (filled($document->attachment_path)) {
|
||||
Storage::disk($document->attachment_disk ?: config('documentation.storage_disk', 'public'))->delete($document->attachment_path);
|
||||
Storage::disk($document->attachment_disk ?: config(self::STORAGE_DISK_CONFIG, 'public'))->delete($document->attachment_path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $moduleNamespace = 'Modules\Documentation\Http\Controllers';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</div>
|
||||
<div class="doc-card-body">
|
||||
<table class="doc-table">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
</form>
|
||||
|
||||
<table class="doc-table">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
|
||||
@@ -6,6 +6,8 @@ use Illuminate\Support\Facades\Route;
|
||||
use Modules\Documentation\Http\Controllers\DocumentationCategoryController;
|
||||
use Modules\Documentation\Http\Controllers\DocumentationController;
|
||||
|
||||
const DOCUMENT_ROUTE = DOCUMENT_ROUTE;
|
||||
|
||||
Route::middleware(['web', 'auth'])
|
||||
->prefix('documentation')
|
||||
->name('documentation.')
|
||||
@@ -15,13 +17,13 @@ Route::middleware(['web', 'auth'])
|
||||
Route::get('/documents', [DocumentationController::class, 'index'])->name('documents.index');
|
||||
Route::get('/documents/create', [DocumentationController::class, 'create'])->name('documents.create');
|
||||
Route::post('/documents', [DocumentationController::class, 'store'])->name('documents.store');
|
||||
Route::get('/documents/{document}', [DocumentationController::class, 'show'])->name('documents.show');
|
||||
Route::get('/documents/{document}/edit', [DocumentationController::class, 'edit'])->name('documents.edit');
|
||||
Route::put('/documents/{document}', [DocumentationController::class, 'update'])->name('documents.update');
|
||||
Route::delete('/documents/{document}', [DocumentationController::class, 'destroy'])->name('documents.destroy');
|
||||
Route::get('/documents/{document}/download', [DocumentationController::class, 'download'])->name('documents.download');
|
||||
Route::get('/documents/{document}/attachments/{attachment}/download', [DocumentationController::class, 'downloadAttachment'])->name('documents.attachments.download');
|
||||
Route::delete('/documents/{document}/attachments/{attachment}', [DocumentationController::class, 'destroyAttachment'])->name('documents.attachments.destroy');
|
||||
Route::get(DOCUMENT_ROUTE, [DocumentationController::class, 'show'])->name('documents.show');
|
||||
Route::get(DOCUMENT_ROUTE . '/edit', [DocumentationController::class, 'edit'])->name('documents.edit');
|
||||
Route::put(DOCUMENT_ROUTE, [DocumentationController::class, 'update'])->name('documents.update');
|
||||
Route::delete(DOCUMENT_ROUTE, [DocumentationController::class, 'destroy'])->name('documents.destroy');
|
||||
Route::get(DOCUMENT_ROUTE . '/download', [DocumentationController::class, 'download'])->name('documents.download');
|
||||
Route::get(DOCUMENT_ROUTE . '/attachments/{attachment}/download', [DocumentationController::class, 'downloadAttachment'])->name('documents.attachments.download');
|
||||
Route::delete(DOCUMENT_ROUTE . '/attachments/{attachment}', [DocumentationController::class, 'destroyAttachment'])->name('documents.attachments.destroy');
|
||||
|
||||
Route::resource('categories', DocumentationCategoryController::class)->except(['show']);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class TicketPersistenceException extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -15,6 +15,13 @@ use Modules\Helpdesk\Services\GitWebhookIngestionService;
|
||||
|
||||
final class GitWebhookController
|
||||
{
|
||||
private const REJECTED = 'rejected';
|
||||
|
||||
private const SIGNATURE_FAILED = 'Git webhook signature or token validation failed.';
|
||||
|
||||
private const SHA256_PREFIX = 'sha256=';
|
||||
|
||||
private const SHA1_PREFIX = 'sha1=';
|
||||
/**
|
||||
* Ingest a signed Git provider webhook for a registered Helpdesk repository.
|
||||
*/
|
||||
@@ -26,14 +33,14 @@ final class GitWebhookController
|
||||
$rawPayload = $request->getContent();
|
||||
|
||||
if (! $repository->is_active) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Repository is inactive.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$result = $this->rejectedResult('Repository is inactive.');
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
||||
|
||||
return response()->json($result, 403);
|
||||
}
|
||||
|
||||
if (! $this->hasValidSignature($request, $repository, $provider, $rawPayload)) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Git webhook signature or token validation failed.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$result = $this->rejectedResult(self::SIGNATURE_FAILED);
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
||||
|
||||
return response()->json($result, 403);
|
||||
@@ -43,7 +50,7 @@ final class GitWebhookController
|
||||
/** @var array<string, mixed> $payload */
|
||||
$payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Invalid JSON webhook payload.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$result = $this->rejectedResult('Invalid JSON webhook payload.');
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result);
|
||||
|
||||
return response()->json($result, 400);
|
||||
@@ -68,29 +75,20 @@ final class GitWebhookController
|
||||
/** @var array<string, mixed> $payload */
|
||||
$payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
return response()->json([
|
||||
'status' => 'rejected',
|
||||
'message' => 'Invalid JSON webhook payload.',
|
||||
'commit_count' => 0,
|
||||
'linked_count' => 0,
|
||||
'ignored_count' => 1,
|
||||
], 400);
|
||||
return response()->json($this->rejectedResult('Invalid JSON webhook payload.'), 400);
|
||||
}
|
||||
|
||||
$repository = $this->repositoryForGiteaPayload($payload);
|
||||
|
||||
if (! $repository instanceof GitRepository) {
|
||||
return response()->json([
|
||||
'status' => 'rejected',
|
||||
'message' => 'No active Helpdesk Git repository matched the Gitea webhook payload.',
|
||||
'commit_count' => 0,
|
||||
'linked_count' => 0,
|
||||
'ignored_count' => 1,
|
||||
], 404);
|
||||
return response()->json(
|
||||
$this->rejectedResult('No active Helpdesk Git repository matched the Gitea webhook payload.'),
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->hasValidGiteaSecurity($request, $repository, $provider, $rawPayload)) {
|
||||
$result = ['status' => 'rejected', 'message' => 'Git webhook signature or token validation failed.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1];
|
||||
$result = $this->rejectedResult(self::SIGNATURE_FAILED);
|
||||
$service->logWebhook($repository, $provider, $deliveryId, $event, $result, $payload);
|
||||
|
||||
return response()->json($result, 403);
|
||||
@@ -143,8 +141,8 @@ final class GitWebhookController
|
||||
$sha256Signature = (string) $request->header('X-Hub-Signature-256', '');
|
||||
$sha1Signature = (string) $request->header('X-Hub-Signature', '');
|
||||
|
||||
return hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $sha256Signature)
|
||||
|| hash_equals('sha1=' . hash_hmac('sha1', $rawPayload, $secret), $sha1Signature);
|
||||
return hash_equals(self::SHA256_PREFIX . hash_hmac('sha256', $rawPayload, $secret), $sha256Signature)
|
||||
|| hash_equals(self::SHA1_PREFIX . hash_hmac('sha1', $rawPayload, $secret), $sha1Signature);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,8 +156,8 @@ final class GitWebhookController
|
||||
$sha1 = hash_hmac('sha1', $rawPayload, $secret);
|
||||
|
||||
return hash_equals($sha256, $giteaSignature)
|
||||
|| hash_equals('sha256=' . $sha256, $giteaSignature)
|
||||
|| hash_equals('sha1=' . $sha1, $giteaSignature)
|
||||
|| hash_equals(self::SHA256_PREFIX . $sha256, $giteaSignature)
|
||||
|| hash_equals(self::SHA1_PREFIX . $sha1, $giteaSignature)
|
||||
|| $this->hasValidGitHubSignature($request, $secret, $rawPayload)
|
||||
|| $this->hasValidGenericSignature($request, $secret, $rawPayload);
|
||||
}
|
||||
@@ -171,7 +169,7 @@ final class GitWebhookController
|
||||
{
|
||||
$signature = (string) $request->header('X-Hub-Signature', '');
|
||||
|
||||
return hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
return hash_equals(self::SHA256_PREFIX . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
|| hash_equals($secret, (string) $request->header('X-Helpdesk-Token', ''));
|
||||
}
|
||||
|
||||
@@ -184,8 +182,8 @@ final class GitWebhookController
|
||||
$signature = (string) ($request->header('X-Hub-Signature-256') ?: $request->header('X-Hub-Signature'));
|
||||
|
||||
return hash_equals($secret, $token)
|
||||
|| hash_equals('sha256=' . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
|| hash_equals('sha1=' . hash_hmac('sha1', $rawPayload, $secret), $signature);
|
||||
|| hash_equals(self::SHA256_PREFIX . hash_hmac('sha256', $rawPayload, $secret), $signature)
|
||||
|| hash_equals(self::SHA1_PREFIX . hash_hmac('sha1', $rawPayload, $secret), $signature);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,6 +323,18 @@ final class GitWebhookController
|
||||
return implode('/', array_slice($segments, -2));
|
||||
}
|
||||
|
||||
/** @return array{status: string, message: string, commit_count: int, linked_count: int, ignored_count: int} */
|
||||
private function rejectedResult(string $message): array
|
||||
{
|
||||
return [
|
||||
'status' => self::REJECTED,
|
||||
'message' => $message,
|
||||
'commit_count' => 0,
|
||||
'linked_count' => 0,
|
||||
'ignored_count' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the event name from provider-specific headers.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Helpdesk\Exceptions\TicketPersistenceException;
|
||||
use Modules\Helpdesk\Models\KanbanBoard;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
@@ -68,7 +69,7 @@ final class TicketBoardController extends Controller
|
||||
->update($updates);
|
||||
|
||||
if ($affected < 1) {
|
||||
throw new \RuntimeException('Ticket board update did not persist.');
|
||||
throw new TicketPersistenceException('Ticket board update did not persist.');
|
||||
}
|
||||
|
||||
return Ticket::query()
|
||||
|
||||
@@ -25,51 +25,53 @@ final class TicketCommentDeleteController extends Controller
|
||||
abort_unless((int) $comment->ticket_id === (int) $ticket->id, 404);
|
||||
|
||||
DB::transaction(function () use ($comment, $ticket, $user): void {
|
||||
$updates = [];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'tombstoned_at')) {
|
||||
$updates['tombstoned_at'] = now();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'deleted_by')) {
|
||||
$updates['deleted_by'] = (int) $user->id;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'delete_reason')) {
|
||||
$updates['delete_reason'] = 'Deleted by MPBA platform administrator.';
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'sync_sequence')) {
|
||||
$updates['sync_sequence'] = ((int) ($comment->sync_sequence ?? 0)) + 1;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'sync_hash')) {
|
||||
$updates['sync_hash'] = hash('sha256', implode('|', [
|
||||
(string) ($comment->guid ?? $comment->id),
|
||||
'deleted',
|
||||
now()->toISOString(),
|
||||
]));
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'updated_at')) {
|
||||
$updates['updated_at'] = now();
|
||||
}
|
||||
|
||||
if ($updates !== []) {
|
||||
DB::table('helpdesk_comments')
|
||||
->where('id', $comment->id)
|
||||
->update($updates);
|
||||
} else {
|
||||
$comment->delete();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
}
|
||||
$this->deleteComment($comment, (int) $user->id);
|
||||
$this->touchTicket($ticket);
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $ticket)
|
||||
->with('ticket-comment-deleted', 'Response deleted.');
|
||||
}
|
||||
|
||||
private function deleteComment(Comment $comment, int $userId): void
|
||||
{
|
||||
$updates = $this->tombstoneUpdates($comment, $userId);
|
||||
|
||||
if ($updates === []) {
|
||||
$comment->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_comments')->where('id', $comment->id)->update($updates);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function tombstoneUpdates(Comment $comment, int $userId): array
|
||||
{
|
||||
$now = now();
|
||||
$candidates = [
|
||||
'tombstoned_at' => $now,
|
||||
'deleted_by' => $userId,
|
||||
'delete_reason' => 'Deleted by MPBA platform administrator.',
|
||||
'sync_sequence' => ((int) ($comment->sync_sequence ?? 0)) + 1,
|
||||
'sync_hash' => hash('sha256', implode('|', [(string) ($comment->guid ?? $comment->id), 'deleted', $now->toISOString()])),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
return array_filter(
|
||||
$candidates,
|
||||
static fn (mixed $value, string $column): bool => Schema::hasColumn('helpdesk_comments', $column),
|
||||
ARRAY_FILTER_USE_BOTH,
|
||||
);
|
||||
}
|
||||
|
||||
private function touchTicket(Ticket $ticket): void
|
||||
{
|
||||
if (Schema::hasColumn('helpdesk_tickets', 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,62 +35,72 @@ final class TicketTimeLogController extends Controller
|
||||
$loggedAt = now();
|
||||
|
||||
DB::transaction(function () use ($ticket, $request, $minutes, $description, $loggedAt): void {
|
||||
if (Schema::hasColumn($ticket->getTable(), 'time_spent_minutes')) {
|
||||
$ticket->increment('time_spent_minutes', $minutes, [
|
||||
'last_activity_at' => $loggedAt,
|
||||
]);
|
||||
} elseif (Schema::hasColumn($ticket->getTable(), 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => $loggedAt])->save();
|
||||
}
|
||||
|
||||
if (Schema::hasTable('helpdesk_time_logs')) {
|
||||
DB::table('helpdesk_time_logs')->insert([
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'logged_at' => $loggedAt,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
if (Schema::hasTable('helpdesk_ticket_time_logs')) {
|
||||
$payload = [
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'started_at')) {
|
||||
$payload['started_at'] = $loggedAt->copy()->subMinutes($minutes);
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'ended_at')) {
|
||||
$payload['ended_at'] = $loggedAt;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'logged_at')) {
|
||||
$payload['logged_at'] = $loggedAt;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'duration_seconds')) {
|
||||
$payload['duration_seconds'] = $minutes * 60;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', 'seconds')) {
|
||||
$payload['seconds'] = $minutes * 60;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_ticket_time_logs')->insert($payload);
|
||||
}
|
||||
$this->updateTicketActivity($ticket, $minutes, $loggedAt);
|
||||
$this->storeLegacyTimeLog($ticket, $request, $minutes, $description, $loggedAt);
|
||||
$this->storeTicketTimeLog($ticket, $request, $minutes, $description, $loggedAt);
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('helpdesk.tickets.show', $ticket)
|
||||
->with('ticket-action-updated', 'Time logged successfully.');
|
||||
}
|
||||
|
||||
private function updateTicketActivity(Ticket $ticket, int $minutes, $loggedAt): void
|
||||
{
|
||||
if (Schema::hasColumn($ticket->getTable(), 'time_spent_minutes')) {
|
||||
$ticket->increment('time_spent_minutes', $minutes, ['last_activity_at' => $loggedAt]);
|
||||
} elseif (Schema::hasColumn($ticket->getTable(), 'last_activity_at')) {
|
||||
$ticket->forceFill(['last_activity_at' => $loggedAt])->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function storeLegacyTimeLog(Ticket $ticket, Request $request, int $minutes, ?string $description, $loggedAt): void
|
||||
{
|
||||
if (! Schema::hasTable('helpdesk_time_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('helpdesk_time_logs')->insert([
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'logged_at' => $loggedAt,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
private function storeTicketTimeLog(Ticket $ticket, Request $request, int $minutes, ?string $description, $loggedAt): void
|
||||
{
|
||||
if (! Schema::hasTable('helpdesk_ticket_time_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'ticket_id' => $ticket->getKey(),
|
||||
'user_id' => $request->user()->getKey(),
|
||||
'minutes' => $minutes,
|
||||
'description' => $description,
|
||||
'created_at' => $loggedAt,
|
||||
'updated_at' => $loggedAt,
|
||||
];
|
||||
|
||||
$optional = [
|
||||
'started_at' => $loggedAt->copy()->subMinutes($minutes),
|
||||
'ended_at' => $loggedAt,
|
||||
'logged_at' => $loggedAt,
|
||||
'duration_seconds' => $minutes * 60,
|
||||
'seconds' => $minutes * 60,
|
||||
];
|
||||
|
||||
foreach ($optional as $column => $value) {
|
||||
if (Schema::hasColumn('helpdesk_ticket_time_logs', $column)) {
|
||||
$payload[$column] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('helpdesk_ticket_time_logs')->insert($payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -156,8 +156,7 @@ final class KanbanBoard extends Component
|
||||
*/
|
||||
public function selectBoard(int|string|null $value): void
|
||||
{
|
||||
$this->board_id = is_numeric($value) ? (int) $value : null;
|
||||
$this->refreshBoard();
|
||||
$this->updatedBoardId($value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,19 @@ use Modules\Helpdesk\Support\Color;
|
||||
|
||||
final class SettingsManager extends Component
|
||||
{
|
||||
private const RULE_MAX_40 = self::RULE_MAX_40;
|
||||
private const RULE_MAX_80 = self::RULE_MAX_80;
|
||||
private const RULE_MAX_100 = self::RULE_MAX_100;
|
||||
private const RULE_MAX_120 = self::RULE_MAX_120;
|
||||
private const RULE_MAX_160 = self::RULE_MAX_160;
|
||||
private const RULE_MAX_180 = self::RULE_MAX_180;
|
||||
private const RULE_MAX_220 = self::RULE_MAX_220;
|
||||
private const RULE_MAX_500 = self::RULE_MAX_500;
|
||||
private const RULE_MIN_0 = self::RULE_MIN_0;
|
||||
private const RULE_TEAM_EXISTS = self::RULE_TEAM_EXISTS;
|
||||
private const RULE_PROJECT_EXISTS = self::RULE_PROJECT_EXISTS;
|
||||
private const RULE_CATEGORY_EXISTS = self::RULE_CATEGORY_EXISTS;
|
||||
|
||||
public string $section = 'statuses';
|
||||
|
||||
public int|string|null $default_helpdesk_company_id = null;
|
||||
@@ -126,10 +139,10 @@ final class SettingsManager extends Component
|
||||
public function saveStatus(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
'statusForm.name' => ['required', 'string', 'max:80'],
|
||||
'statusForm.slug' => ['nullable', 'string', 'max:100'],
|
||||
'statusForm.name' => ['required', 'string', self::RULE_MAX_80],
|
||||
'statusForm.slug' => ['nullable', 'string', self::RULE_MAX_100],
|
||||
'statusForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'statusForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'statusForm.sort_order' => ['required', 'integer', self::RULE_MIN_0],
|
||||
'statusForm.is_closed' => ['boolean'],
|
||||
'statusForm.is_default' => ['boolean'],
|
||||
])['statusForm'];
|
||||
@@ -167,10 +180,10 @@ final class SettingsManager extends Component
|
||||
public function savePriority(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
'priorityForm.name' => ['required', 'string', 'max:80'],
|
||||
'priorityForm.slug' => ['nullable', 'string', 'max:100'],
|
||||
'priorityForm.name' => ['required', 'string', self::RULE_MAX_80],
|
||||
'priorityForm.slug' => ['nullable', 'string', self::RULE_MAX_100],
|
||||
'priorityForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'priorityForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'priorityForm.sort_order' => ['required', 'integer', self::RULE_MIN_0],
|
||||
'priorityForm.is_default' => ['boolean'],
|
||||
])['priorityForm'];
|
||||
|
||||
@@ -209,7 +222,7 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'categoryForm.company_id' => ['nullable', 'integer'],
|
||||
'categoryForm.name' => ['required', 'string', 'max:120'],
|
||||
'categoryForm.name' => ['required', 'string', self::RULE_MAX_120],
|
||||
'categoryForm.slug' => ['nullable', 'string', 'max:140'],
|
||||
'categoryForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'categoryForm.description' => ['nullable', 'string'],
|
||||
@@ -251,7 +264,7 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'teamForm.company_id' => ['nullable', 'integer'],
|
||||
'teamForm.name' => ['required', 'string', 'max:120'],
|
||||
'teamForm.name' => ['required', 'string', self::RULE_MAX_120],
|
||||
'teamForm.slug' => ['nullable', 'string', 'max:140'],
|
||||
'teamForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'teamForm.description' => ['nullable', 'string'],
|
||||
@@ -293,9 +306,9 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'projectForm.company_id' => ['nullable', 'integer'],
|
||||
'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
|
||||
'projectForm.title' => ['required', 'string', 'max:160'],
|
||||
'projectForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'projectForm.team_id' => ['nullable', 'integer', self::RULE_TEAM_EXISTS],
|
||||
'projectForm.title' => ['required', 'string', self::RULE_MAX_160],
|
||||
'projectForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'projectForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'projectForm.description' => ['nullable', 'string'],
|
||||
'projectForm.is_default' => ['boolean'],
|
||||
@@ -335,12 +348,12 @@ final class SettingsManager extends Component
|
||||
$this->normalizeNullableFormValues('boardForm', ['owner_id']);
|
||||
|
||||
$data = $this->validate([
|
||||
'boardForm.name' => ['required', 'string', 'max:160'],
|
||||
'boardForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'boardForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'boardForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'boardForm.description' => ['nullable', 'string'],
|
||||
'boardForm.owner_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'boardForm.is_default' => ['boolean'],
|
||||
'boardForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'boardForm.sort_order' => ['required', 'integer', self::RULE_MIN_0],
|
||||
'boardForm.status_ids' => ['array'],
|
||||
'boardForm.status_ids.*' => ['integer', 'exists:helpdesk_statuses,id'],
|
||||
])['boardForm'];
|
||||
@@ -403,12 +416,12 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'repositoryForm.company_id' => ['nullable', 'integer'],
|
||||
'repositoryForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
|
||||
'repositoryForm.name' => ['required', 'string', 'max:160'],
|
||||
'repositoryForm.provider' => ['required', 'string', 'max:40'],
|
||||
'repositoryForm.remote_url' => ['required', 'string', 'max:500'],
|
||||
'repositoryForm.default_branch' => ['required', 'string', 'max:120'],
|
||||
'repositoryForm.webhook_secret' => ['nullable', 'string', 'max:120'],
|
||||
'repositoryForm.project_id' => ['nullable', 'integer', self::RULE_PROJECT_EXISTS],
|
||||
'repositoryForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'repositoryForm.provider' => ['required', 'string', self::RULE_MAX_40],
|
||||
'repositoryForm.remote_url' => ['required', 'string', self::RULE_MAX_500],
|
||||
'repositoryForm.default_branch' => ['required', 'string', self::RULE_MAX_120],
|
||||
'repositoryForm.webhook_secret' => ['nullable', 'string', self::RULE_MAX_120],
|
||||
])['repositoryForm'];
|
||||
|
||||
$data['provider'] = strtolower(trim((string) $data['provider']));
|
||||
@@ -440,14 +453,14 @@ final class SettingsManager extends Component
|
||||
$this->normalizeNullableFormValues('workflowForm', ['text_body', 'cc', 'bcc']);
|
||||
|
||||
$data = $this->validate([
|
||||
'workflowForm.name' => ['required', 'string', 'max:160'],
|
||||
'workflowForm.event' => ['required', 'string', 'max:80'],
|
||||
'workflowForm.subject' => ['required', 'string', 'max:220'],
|
||||
'workflowForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'workflowForm.event' => ['required', 'string', self::RULE_MAX_80],
|
||||
'workflowForm.subject' => ['required', 'string', self::RULE_MAX_220],
|
||||
'workflowForm.html_body' => ['required', 'string'],
|
||||
'workflowForm.text_body' => ['nullable', 'string'],
|
||||
'workflowForm.to_role' => ['required', 'string', 'max:40'],
|
||||
'workflowForm.cc' => ['nullable', 'string', 'max:500'],
|
||||
'workflowForm.bcc' => ['nullable', 'string', 'max:500'],
|
||||
'workflowForm.to_role' => ['required', 'string', self::RULE_MAX_40],
|
||||
'workflowForm.cc' => ['nullable', 'string', self::RULE_MAX_500],
|
||||
'workflowForm.bcc' => ['nullable', 'string', self::RULE_MAX_500],
|
||||
'workflowForm.is_active' => ['boolean'],
|
||||
])['workflowForm'];
|
||||
|
||||
@@ -479,15 +492,15 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'cannedForm.company_id' => ['nullable', 'integer'],
|
||||
'cannedForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
'cannedForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
|
||||
'cannedForm.name' => ['required', 'string', 'max:160'],
|
||||
'cannedForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'cannedForm.visibility' => ['required', 'string', 'max:40'],
|
||||
'cannedForm.subject' => ['nullable', 'string', 'max:220'],
|
||||
'cannedForm.category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS],
|
||||
'cannedForm.team_id' => ['nullable', 'integer', self::RULE_TEAM_EXISTS],
|
||||
'cannedForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'cannedForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'cannedForm.visibility' => ['required', 'string', self::RULE_MAX_40],
|
||||
'cannedForm.subject' => ['nullable', 'string', self::RULE_MAX_220],
|
||||
'cannedForm.body' => ['required', 'string'],
|
||||
'cannedForm.is_active' => ['boolean'],
|
||||
'cannedForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'cannedForm.sort_order' => ['required', 'integer', self::RULE_MIN_0],
|
||||
])['cannedForm'];
|
||||
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
||||
$this->persistSettingModel(CannedResponse::class, $this->cannedForm['id'], $data);
|
||||
@@ -519,9 +532,9 @@ final class SettingsManager extends Component
|
||||
$data = $this->validate([
|
||||
'slaForm.company_id' => ['nullable', 'integer'],
|
||||
'slaForm.priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'],
|
||||
'slaForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
'slaForm.name' => ['required', 'string', 'max:160'],
|
||||
'slaForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'slaForm.category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS],
|
||||
'slaForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'slaForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'slaForm.first_response_minutes' => ['required', 'integer', 'min:1'],
|
||||
'slaForm.resolution_minutes' => ['required', 'integer', 'min:1'],
|
||||
'slaForm.business_hours_only' => ['boolean'],
|
||||
@@ -560,20 +573,20 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'automationForm.company_id' => ['nullable', 'integer'],
|
||||
'automationForm.name' => ['required', 'string', 'max:160'],
|
||||
'automationForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'automationForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'automationForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'automationForm.match_sender' => ['nullable', 'string', 'max:190'],
|
||||
'automationForm.match_subject' => ['nullable', 'string', 'max:220'],
|
||||
'automationForm.match_subject' => ['nullable', 'string', self::RULE_MAX_220],
|
||||
'automationForm.match_mailbox' => ['nullable', 'string', 'max:190'],
|
||||
'automationForm.match_category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
'automationForm.match_category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS],
|
||||
'automationForm.set_priority_id' => ['nullable', 'integer', 'exists:helpdesk_priorities,id'],
|
||||
'automationForm.set_category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
'automationForm.set_team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
|
||||
'automationForm.set_project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
|
||||
'automationForm.set_category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS],
|
||||
'automationForm.set_team_id' => ['nullable', 'integer', self::RULE_TEAM_EXISTS],
|
||||
'automationForm.set_project_id' => ['nullable', 'integer', self::RULE_PROJECT_EXISTS],
|
||||
'automationForm.set_status_id' => ['nullable', 'integer', 'exists:helpdesk_statuses,id'],
|
||||
'automationForm.set_agent_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'automationForm.is_active' => ['boolean'],
|
||||
'automationForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'automationForm.sort_order' => ['required', 'integer', self::RULE_MIN_0],
|
||||
])['automationForm'];
|
||||
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
||||
$this->persistSettingModel(AutomationRule::class, $this->automationForm['id'], $data);
|
||||
@@ -604,11 +617,11 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'productForm.company_id' => ['nullable', 'integer'],
|
||||
'productForm.name' => ['required', 'string', 'max:160'],
|
||||
'productForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'productForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'productForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'productForm.type' => ['required', 'string', 'max:60'],
|
||||
'productForm.version' => ['nullable', 'string', 'max:80'],
|
||||
'productForm.release_channel' => ['nullable', 'string', 'max:80'],
|
||||
'productForm.version' => ['nullable', 'string', self::RULE_MAX_80],
|
||||
'productForm.release_channel' => ['nullable', 'string', self::RULE_MAX_80],
|
||||
'productForm.description' => ['nullable', 'string'],
|
||||
'productForm.is_active' => ['boolean'],
|
||||
])['productForm'];
|
||||
@@ -641,8 +654,8 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'tagForm.company_id' => ['nullable', 'integer'],
|
||||
'tagForm.name' => ['required', 'string', 'max:80'],
|
||||
'tagForm.slug' => ['nullable', 'string', 'max:100'],
|
||||
'tagForm.name' => ['required', 'string', self::RULE_MAX_80],
|
||||
'tagForm.slug' => ['nullable', 'string', self::RULE_MAX_100],
|
||||
'tagForm.color' => ['required', 'string', Color::validationRule()],
|
||||
])['tagForm'];
|
||||
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
||||
@@ -675,10 +688,10 @@ final class SettingsManager extends Component
|
||||
|
||||
$data = $this->validate([
|
||||
'checklistTemplateForm.company_id' => ['nullable', 'integer'],
|
||||
'checklistTemplateForm.category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
|
||||
'checklistTemplateForm.project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
|
||||
'checklistTemplateForm.name' => ['required', 'string', 'max:160'],
|
||||
'checklistTemplateForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'checklistTemplateForm.category_id' => ['nullable', 'integer', self::RULE_CATEGORY_EXISTS],
|
||||
'checklistTemplateForm.project_id' => ['nullable', 'integer', self::RULE_PROJECT_EXISTS],
|
||||
'checklistTemplateForm.name' => ['required', 'string', self::RULE_MAX_160],
|
||||
'checklistTemplateForm.slug' => ['nullable', 'string', self::RULE_MAX_180],
|
||||
'checklistTemplateForm.items_text' => ['required', 'string'],
|
||||
'checklistTemplateForm.is_active' => ['boolean'],
|
||||
])['checklistTemplateForm'];
|
||||
|
||||
@@ -774,12 +774,6 @@ final class TicketShow extends Component
|
||||
/**
|
||||
* Add a case-insensitive LIKE condition that works on PostgreSQL and SQLite tests.
|
||||
*/
|
||||
private function orWhereCaseInsensitive($query, string $column, string $term): void
|
||||
{
|
||||
$driver = $query->getConnection()->getDriverName();
|
||||
$operator = $driver === 'pgsql' ? $this->gitCommitLikeOperator() : 'LIKE';
|
||||
$query->orWhere($column, $operator, '%' . $term . '%');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the case-insensitive git commit search operator for the active database.
|
||||
|
||||
@@ -11,6 +11,8 @@ final class TicketPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
unset($user);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -23,6 +25,8 @@ final class TicketPolicy
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
unset($user);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -648,10 +648,9 @@ span.flatpickr-weekday {
|
||||
gap: .5rem;
|
||||
border-radius: .85rem;
|
||||
padding: .62rem 1.05rem;
|
||||
font-size: .875rem;
|
||||
font-size: .95rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 800;
|
||||
font-size: .95rem;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none !important;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
|
||||
@@ -53,10 +53,14 @@
|
||||
<section class="overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-950 shadow-sm">
|
||||
<div class="overflow-auto">
|
||||
<table class="min-w-full border-collapse font-mono text-xs leading-6">
|
||||
<caption class="sr-only">Source code for {{ $attachmentName }}</caption>
|
||||
<thead class="sr-only">
|
||||
<tr><th scope="col">Line</th><th scope="col">Source</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($lines as $index => $line)
|
||||
<tr class="group hover:bg-zinc-900/80">
|
||||
<td class="select-none border-r border-zinc-800 bg-zinc-900 px-4 text-right text-zinc-500">{{ $index + 1 }}</td>
|
||||
<th scope="row" class="select-none border-r border-zinc-800 bg-zinc-900 px-4 text-right font-normal text-zinc-500">{{ $index + 1 }}</th>
|
||||
<td class="whitespace-pre px-4 text-zinc-100"><code>{{ $line }}</code></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
@@ -543,6 +544,7 @@
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
@@ -709,6 +711,7 @@
|
||||
<div
|
||||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="bg-zinc-50 text-left text-xs font-semibold uppercase text-zinc-500 dark:bg-zinc-800">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Policy</th>
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
|
||||
@@ -1657,7 +1657,7 @@
|
||||
|
||||
<style>
|
||||
[data-helpdesk-sidebar-cards] details > summary::-webkit-details-marker {
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-helpdesk-sidebar-cards] details > summary {
|
||||
|
||||
@@ -114,23 +114,13 @@ final class AttachmentService
|
||||
{
|
||||
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
|
||||
if (str_starts_with($mimeType, 'image/') || in_array($extension, ['png', 'jpg', 'jpeg', 'webp', 'gif'], true)) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if ($extension === 'pdf' || $mimeType === 'application/pdf') {
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
if (in_array($extension, ['zip', 'tar', 'gz', '7z', 'rar'], true)) {
|
||||
return 'archive';
|
||||
}
|
||||
|
||||
if (in_array($extension, config('helpdesk.allowed_source_extensions', []), true)) {
|
||||
return 'source';
|
||||
}
|
||||
|
||||
return 'file';
|
||||
return match (true) {
|
||||
str_starts_with($mimeType, 'image/'), in_array($extension, ['png', 'jpg', 'jpeg', 'webp', 'gif'], true) => 'image',
|
||||
$extension === 'pdf', $mimeType === 'application/pdf' => 'pdf',
|
||||
in_array($extension, ['zip', 'tar', 'gz', '7z', 'rar'], true) => 'archive',
|
||||
in_array($extension, config('helpdesk.allowed_source_extensions', []), true) => 'source',
|
||||
default => 'file',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,18 +29,7 @@ final class AutomationRuleService
|
||||
return;
|
||||
}
|
||||
|
||||
$updates = array_filter([
|
||||
'priority_id' => $rule->set_priority_id,
|
||||
'category_id' => $rule->set_category_id,
|
||||
'team_id' => $rule->set_team_id,
|
||||
'project_id' => $rule->set_project_id,
|
||||
'status_id' => $rule->set_status_id,
|
||||
'agent_id' => $rule->set_agent_id,
|
||||
], static fn ($value): bool => $value !== null);
|
||||
|
||||
if ($updates !== []) {
|
||||
$ticket->forceFill($updates + ['last_activity_at' => now()])->save();
|
||||
}
|
||||
$this->applyRuleUpdates($rule, $ticket);
|
||||
});
|
||||
|
||||
return $ticket->fresh() ?? $ticket;
|
||||
@@ -48,19 +37,30 @@ final class AutomationRuleService
|
||||
|
||||
private function matches(AutomationRule $rule, Ticket $ticket, string $sender, string $mailbox): bool
|
||||
{
|
||||
if ($rule->match_sender && ! Str::contains(Str::lower($sender), Str::lower($rule->match_sender))) {
|
||||
return false;
|
||||
}
|
||||
if ($rule->match_subject && ! Str::contains(Str::lower($ticket->subject), Str::lower($rule->match_subject))) {
|
||||
return false;
|
||||
}
|
||||
if ($rule->match_mailbox && ! Str::contains(Str::lower($mailbox), Str::lower($rule->match_mailbox))) {
|
||||
return false;
|
||||
}
|
||||
if ($rule->match_category_id && (int) $ticket->category_id !== (int) $rule->match_category_id) {
|
||||
return false;
|
||||
}
|
||||
return $this->containsWhenConfigured($sender, $rule->match_sender)
|
||||
&& $this->containsWhenConfigured($ticket->subject, $rule->match_subject)
|
||||
&& $this->containsWhenConfigured($mailbox, $rule->match_mailbox)
|
||||
&& (! $rule->match_category_id || (int) $ticket->category_id === (int) $rule->match_category_id);
|
||||
}
|
||||
|
||||
return true;
|
||||
private function containsWhenConfigured(string $value, ?string $match): bool
|
||||
{
|
||||
return ! $match || Str::contains(Str::lower($value), Str::lower($match));
|
||||
}
|
||||
|
||||
private function applyRuleUpdates(AutomationRule $rule, Ticket $ticket): void
|
||||
{
|
||||
$updates = array_filter([
|
||||
'priority_id' => $rule->set_priority_id,
|
||||
'category_id' => $rule->set_category_id,
|
||||
'team_id' => $rule->set_team_id,
|
||||
'project_id' => $rule->set_project_id,
|
||||
'status_id' => $rule->set_status_id,
|
||||
'agent_id' => $rule->set_agent_id,
|
||||
], static fn ($value): bool => $value !== null);
|
||||
|
||||
if ($updates !== []) {
|
||||
$ticket->forceFill($updates + ['last_activity_at' => now()])->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Helpdesk\Exceptions\TicketPersistenceException;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\KanbanBoard;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
@@ -45,7 +46,7 @@ final class TicketService
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Unable to create a unique Helpdesk ticket reference.');
|
||||
throw new TicketPersistenceException('Unable to create a unique Helpdesk ticket reference.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,32 +57,8 @@ final class TicketService
|
||||
*/
|
||||
private function createInsideTransaction(User $user, array $data, array $files = []): Ticket
|
||||
{
|
||||
$defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new');
|
||||
$status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')
|
||||
? Status::query()->where('is_default', true)->orderBy('sort_order')->first()
|
||||
: null;
|
||||
|
||||
if (! $status) {
|
||||
$status = Status::query()->where('slug', $defaultStatusSlug)->first();
|
||||
}
|
||||
|
||||
if (! $status) {
|
||||
$status = Status::query()->create([
|
||||
'name' => 'New',
|
||||
'slug' => $defaultStatusSlug,
|
||||
'color' => 'sky',
|
||||
'sort_order' => 1,
|
||||
'is_closed' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$boardId = $data['board_id'] ?? null;
|
||||
if (blank($boardId) && Schema::hasTable('helpdesk_kanban_boards')) {
|
||||
$boardId = KanbanBoard::query()
|
||||
->where('is_default', true)
|
||||
->orderBy('sort_order')
|
||||
->value('id') ?: KanbanBoard::query()->orderBy('sort_order')->value('id');
|
||||
}
|
||||
$status = $this->resolveInitialStatus();
|
||||
$boardId = $this->resolveBoardId($data['board_id'] ?? null);
|
||||
|
||||
/** @var Ticket $ticket */
|
||||
$ticket = Ticket::query()->create([
|
||||
@@ -102,9 +79,7 @@ final class TicketService
|
||||
'last_activity_at' => now(),
|
||||
]);
|
||||
|
||||
if (! empty($data['tag_ids']) && is_array($data['tag_ids'])) {
|
||||
$ticket->tags()->sync(array_values(array_unique(array_map('intval', $data['tag_ids']))));
|
||||
}
|
||||
$this->syncTags($ticket, $data['tag_ids'] ?? null);
|
||||
|
||||
$ticket = $this->automation->apply($ticket, $data);
|
||||
$ticket = $this->sla->applyPolicy($ticket);
|
||||
@@ -117,6 +92,43 @@ final class TicketService
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
private function resolveInitialStatus(): Status
|
||||
{
|
||||
$defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new');
|
||||
$status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')
|
||||
? Status::query()->where('is_default', true)->orderBy('sort_order')->first()
|
||||
: null;
|
||||
|
||||
$status ??= Status::query()->where('slug', $defaultStatusSlug)->first();
|
||||
|
||||
return $status ?? Status::query()->create([
|
||||
'name' => 'New',
|
||||
'slug' => $defaultStatusSlug,
|
||||
'color' => 'sky',
|
||||
'sort_order' => 1,
|
||||
'is_closed' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveBoardId(mixed $boardId): mixed
|
||||
{
|
||||
if (! blank($boardId) || ! Schema::hasTable('helpdesk_kanban_boards')) {
|
||||
return $boardId;
|
||||
}
|
||||
|
||||
return KanbanBoard::query()->where('is_default', true)->orderBy('sort_order')->value('id')
|
||||
?: KanbanBoard::query()->orderBy('sort_order')->value('id');
|
||||
}
|
||||
|
||||
private function syncTags(Ticket $ticket, mixed $tagIds): void
|
||||
{
|
||||
if (! is_array($tagIds) || $tagIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ticket->tags()->sync(array_values(array_unique(array_map('intval', $tagIds))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a database error was caused by the ticket reference unique index.
|
||||
*/
|
||||
@@ -137,52 +149,68 @@ final class TicketService
|
||||
public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null, ?string $subject = null): Comment
|
||||
{
|
||||
return DB::transaction(function () use ($ticket, $user, $body, $internal, $files, $parentId, $subject): Comment {
|
||||
$parent = $parentId === null
|
||||
? null
|
||||
: Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->findOrFail($parentId);
|
||||
|
||||
$guid = (string) Str::uuid();
|
||||
$unid = strtoupper(hash('sha256', $guid . '|' . $ticket->id . '|' . microtime(true)));
|
||||
$parentUnid = $parent?->unid;
|
||||
$rootUnid = $parent?->root_unid ?: $parentUnid ?: $unid;
|
||||
$plainTextBody = trim(preg_replace('/\s+/', ' ', strip_tags($body)) ?: '');
|
||||
|
||||
$payload = [
|
||||
'parent_id' => $parentId,
|
||||
'user_id' => $user->id,
|
||||
'body' => $body,
|
||||
'body_text' => $plainTextBody,
|
||||
'is_internal' => $internal,
|
||||
'guid' => $guid,
|
||||
'unid' => $unid,
|
||||
'parent_unid' => $parentUnid,
|
||||
'root_unid' => $rootUnid,
|
||||
'originator_id' => $unid . ':1',
|
||||
'sync_sequence' => 1,
|
||||
'sync_hash' => hash('sha256', $body),
|
||||
'source_system' => 'wms-helpdesk',
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'subject')) {
|
||||
$payload['subject'] = trim((string) ($subject ?: ($parentId === null ? 'Response' : 'RE: Response')));
|
||||
}
|
||||
$parent = $this->resolveParentComment($ticket, $parentId);
|
||||
$payload = $this->commentPayload($ticket, $user, $body, $internal, $parent, $parentId, $subject);
|
||||
|
||||
/** @var Comment $comment */
|
||||
$comment = $ticket->comments()->create($payload);
|
||||
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
if (! $internal) {
|
||||
$this->sla->recordPublicResponse($ticket, $user);
|
||||
}
|
||||
$this->attachments->storeMany($ticket, $files, (int) $user->id, $comment);
|
||||
$this->workflow->dispatch($ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket, $parentId === null ? 'ticket.commented' : 'ticket.reply_created', $user);
|
||||
$this->afterCommentCreated($ticket, $user, $comment, $files, $internal, $parentId);
|
||||
|
||||
return $comment->fresh(['user', 'attachments']) ?? $comment;
|
||||
});
|
||||
}
|
||||
|
||||
private function resolveParentComment(Ticket $ticket, ?int $parentId): ?Comment
|
||||
{
|
||||
return $parentId === null
|
||||
? null
|
||||
: Comment::query()->where('ticket_id', $ticket->id)->findOrFail($parentId);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function commentPayload(Ticket $ticket, User $user, string $body, bool $internal, ?Comment $parent, ?int $parentId, ?string $subject): array
|
||||
{
|
||||
$guid = (string) Str::uuid();
|
||||
$unid = strtoupper(hash('sha256', $guid . '|' . $ticket->id . '|' . microtime(true)));
|
||||
$parentUnid = $parent?->unid;
|
||||
$payload = [
|
||||
'parent_id' => $parentId,
|
||||
'user_id' => $user->id,
|
||||
'body' => $body,
|
||||
'body_text' => trim(preg_replace('/\s+/', ' ', strip_tags($body)) ?: ''),
|
||||
'is_internal' => $internal,
|
||||
'guid' => $guid,
|
||||
'unid' => $unid,
|
||||
'parent_unid' => $parentUnid,
|
||||
'root_unid' => $parent?->root_unid ?: $parentUnid ?: $unid,
|
||||
'originator_id' => $unid . ':1',
|
||||
'sync_sequence' => 1,
|
||||
'sync_hash' => hash('sha256', $body),
|
||||
'source_system' => 'wms-helpdesk',
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('helpdesk_comments', 'subject')) {
|
||||
$payload['subject'] = trim((string) ($subject ?: ($parentId === null ? 'Response' : 'RE: Response')));
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/** @param array<int, UploadedFile> $files */
|
||||
private function afterCommentCreated(Ticket $ticket, User $user, Comment $comment, array $files, bool $internal, ?int $parentId): void
|
||||
{
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
|
||||
if (! $internal) {
|
||||
$this->sla->recordPublicResponse($ticket, $user);
|
||||
}
|
||||
|
||||
$this->attachments->storeMany($ticket, $files, (int) $user->id, $comment);
|
||||
$event = $parentId === null ? 'ticket.commented' : 'ticket.reply_created';
|
||||
$freshTicket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket;
|
||||
$this->workflow->dispatch($freshTicket, $event, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a ticket.
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,6 @@ final class WorkflowNotificationService
|
||||
$trackingToken = Str::random(48);
|
||||
$subject = $this->renderText($template->subject, $ticket, $actor);
|
||||
$html = $this->wrapHtml($this->renderText($template->html_body, $ticket, $actor), $ticket, $trackingToken);
|
||||
$text = $this->renderText($template->text_body ?: strip_tags($template->html_body), $ticket, $actor);
|
||||
|
||||
try {
|
||||
Mail::html($html, function ($message) use ($recipient, $template, $subject): void {
|
||||
|
||||
@@ -22,14 +22,12 @@ trait BuildsHelpdeskFixtures
|
||||
{
|
||||
protected function user(array $attributes = []): User
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create(array_merge([
|
||||
/** @var User */
|
||||
return User::factory()->create(array_merge([
|
||||
'name' => 'Helpdesk User',
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'password' => Hash::make('password'),
|
||||
], $attributes));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function agent(array $attributes = []): User
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Part Number</th>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
return [
|
||||
'name' => 'M3',
|
||||
'features' =>
|
||||
[
|
||||
],
|
||||
'features' => [
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Modules\M3\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class M3DatabaseSeeder extends Seeder
|
||||
{
|
||||
@@ -16,6 +16,5 @@ class M3DatabaseSeeder extends Seeder
|
||||
{
|
||||
Model::unguard();
|
||||
|
||||
// $this->call("OthersTableSeeder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Modules\M3\Support\ModuleInfo;
|
||||
|
||||
if (! function_exists('m3_feature_enabled')) {
|
||||
@@ -7,6 +8,6 @@ if (! function_exists('m3_feature_enabled')) {
|
||||
*/
|
||||
function m3_feature_enabled(string $key): bool
|
||||
{
|
||||
return (bool) config(ModuleInfo::nameLower().".features.{$key}", false);
|
||||
return (bool) config(ModuleInfo::nameLower() . ".features.{$key}", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class M3Controller extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function index()
|
||||
@@ -19,6 +20,7 @@ class M3Controller extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function create()
|
||||
@@ -28,7 +30,9 @@ class M3Controller extends Controller
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function store(Request $request)
|
||||
@@ -38,7 +42,9 @@ class M3Controller extends Controller
|
||||
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function show($id)
|
||||
@@ -48,7 +54,9 @@ class M3Controller extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function edit($id)
|
||||
@@ -58,8 +66,10 @@ class M3Controller extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
@@ -69,7 +79,9 @@ class M3Controller extends Controller
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function destroy($id)
|
||||
|
||||
@@ -2,31 +2,29 @@
|
||||
|
||||
namespace Modules\M3\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Database\Eloquent\Factory;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\M3\Support\ModuleInfo;
|
||||
use Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\M3\Support\ModuleInfo;
|
||||
|
||||
class M3ServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* @var string $moduleName
|
||||
* @var string
|
||||
*/
|
||||
protected string $moduleName = 'M3';
|
||||
|
||||
/**
|
||||
* @var string $moduleNameLower
|
||||
* @var string
|
||||
*/
|
||||
protected string $moduleNameLower = 'm3';
|
||||
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides():array
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -36,7 +34,7 @@ class M3ServiceProvider extends ServiceProvider
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot() : void
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
@@ -63,34 +61,33 @@ class M3ServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected function registerHelpers(): void
|
||||
{
|
||||
foreach (glob(__DIR__.'/../Helpers/*.php') as $filename) {
|
||||
foreach (glob(__DIR__ . '/../Helpers/*.php') as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Dynamically Register Commands.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerCommands(): void
|
||||
{
|
||||
if (!$this->app->runningInConsole()) {
|
||||
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);
|
||||
{
|
||||
if (! $this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
$commandPath = __DIR__ . '/../Console';
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(function ($file) {
|
||||
return 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
})
|
||||
->filter(function ($class) {
|
||||
return class_exists($class);
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
$this->commands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
@@ -100,10 +97,11 @@ class M3ServiceProvider extends ServiceProvider
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$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');
|
||||
$this->mergeConfigFrom(
|
||||
module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()
|
||||
module_path(ModuleInfo::name(), 'Config/config.php'),
|
||||
ModuleInfo::nameLower(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,9 +115,9 @@ class M3ServiceProvider extends ServiceProvider
|
||||
$viewPath = resource_path('views/modules/' . ModuleInfo::nameLower());
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath
|
||||
$sourcePath => $viewPath,
|
||||
], ['views', ModuleInfo::nameLower() . '-module-views']);
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,10 +127,10 @@ class M3ServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
|
||||
$langPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
|
||||
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
|
||||
}
|
||||
@@ -140,6 +138,7 @@ class M3ServiceProvider extends ServiceProvider
|
||||
|
||||
/**
|
||||
* Regsiter View Paths
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getPublishableViewPaths(): array
|
||||
@@ -150,6 +149,7 @@ class M3ServiceProvider extends ServiceProvider
|
||||
$paths[] = $path . '/modules/' . ModuleInfo::nameLower();
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Modules\M3\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -14,17 +14,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $moduleNamespace = 'Modules\M3\Http\Controllers';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
|
||||
@@ -15,4 +15,4 @@ use Illuminate\Http\Request;
|
||||
|
||||
Route::middleware('auth:api')->get('/m3', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
|
|
||||
*/
|
||||
|
||||
Route::prefix('m3')->group(function() {
|
||||
Route::prefix('m3')->group(function () {
|
||||
Route::get('/', 'M3Controller@index');
|
||||
});
|
||||
|
||||
@@ -4,10 +4,11 @@ namespace Modules\M3\Support;
|
||||
|
||||
class ModuleInfo
|
||||
{
|
||||
protected static string $moduleName = 'M3';
|
||||
protected static string $moduleName = 'M3';
|
||||
|
||||
/**
|
||||
* defines the Module Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function name(): string
|
||||
@@ -17,6 +18,7 @@ class ModuleInfo
|
||||
|
||||
/**
|
||||
* define the lowercase version
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function nameLower(): string
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Transaction</th>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
return [
|
||||
'name' => 'OTM',
|
||||
'features' =>
|
||||
[
|
||||
],
|
||||
'features' => [
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Modules\OTM\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class OTMDatabaseSeeder extends Seeder
|
||||
{
|
||||
@@ -16,6 +16,5 @@ class OTMDatabaseSeeder extends Seeder
|
||||
{
|
||||
Model::unguard();
|
||||
|
||||
// $this->call("OthersTableSeeder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Modules\OTM\Support\ModuleInfo;
|
||||
|
||||
if (! function_exists('otm_feature_enabled')) {
|
||||
@@ -7,6 +8,6 @@ if (! function_exists('otm_feature_enabled')) {
|
||||
*/
|
||||
function otm_feature_enabled(string $key): bool
|
||||
{
|
||||
return (bool) config(ModuleInfo::nameLower().".features.{$key}", false);
|
||||
return (bool) config(ModuleInfo::nameLower() . ".features.{$key}", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class OTMController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function index()
|
||||
@@ -19,6 +20,7 @@ class OTMController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function create()
|
||||
@@ -28,7 +30,9 @@ class OTMController extends Controller
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function store(Request $request)
|
||||
@@ -38,7 +42,9 @@ class OTMController extends Controller
|
||||
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function show($id)
|
||||
@@ -48,7 +54,9 @@ class OTMController extends Controller
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function edit($id)
|
||||
@@ -58,8 +66,10 @@ class OTMController extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
@@ -69,7 +79,9 @@ class OTMController extends Controller
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return Renderable
|
||||
*/
|
||||
public function destroy($id)
|
||||
|
||||
@@ -2,31 +2,29 @@
|
||||
|
||||
namespace Modules\OTM\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Database\Eloquent\Factory;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\OTM\Support\ModuleInfo;
|
||||
use Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\OTM\Support\ModuleInfo;
|
||||
|
||||
class OTMServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* @var string $moduleName
|
||||
* @var string
|
||||
*/
|
||||
protected string $moduleName = 'OTM';
|
||||
|
||||
/**
|
||||
* @var string $moduleNameLower
|
||||
* @var string
|
||||
*/
|
||||
protected string $moduleNameLower = 'otm';
|
||||
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides():array
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -36,7 +34,7 @@ class OTMServiceProvider extends ServiceProvider
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot() : void
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
@@ -63,34 +61,33 @@ class OTMServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected function registerHelpers(): void
|
||||
{
|
||||
foreach (glob(__DIR__.'/../Helpers/*.php') as $filename) {
|
||||
foreach (glob(__DIR__ . '/../Helpers/*.php') as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Dynamically Register Commands.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerCommands(): void
|
||||
{
|
||||
if (!$this->app->runningInConsole()) {
|
||||
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);
|
||||
{
|
||||
if (! $this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
$commandPath = __DIR__ . '/../Console';
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(function ($file) {
|
||||
return 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
})
|
||||
->filter(function ($class) {
|
||||
return class_exists($class);
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
$this->commands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
@@ -100,10 +97,11 @@ class OTMServiceProvider extends ServiceProvider
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$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');
|
||||
$this->mergeConfigFrom(
|
||||
module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()
|
||||
module_path(ModuleInfo::name(), 'Config/config.php'),
|
||||
ModuleInfo::nameLower(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,9 +115,9 @@ class OTMServiceProvider extends ServiceProvider
|
||||
$viewPath = resource_path('views/modules/' . ModuleInfo::nameLower());
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath
|
||||
$sourcePath => $viewPath,
|
||||
], ['views', ModuleInfo::nameLower() . '-module-views']);
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,10 +127,10 @@ class OTMServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
|
||||
$langPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
|
||||
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
|
||||
}
|
||||
@@ -140,6 +138,7 @@ class OTMServiceProvider extends ServiceProvider
|
||||
|
||||
/**
|
||||
* Regsiter View Paths
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getPublishableViewPaths(): array
|
||||
@@ -150,6 +149,7 @@ class OTMServiceProvider extends ServiceProvider
|
||||
$paths[] = $path . '/modules/' . ModuleInfo::nameLower();
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Modules\OTM\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -14,17 +14,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $moduleNamespace = 'Modules\OTM\Http\Controllers';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
|
||||
@@ -15,4 +15,4 @@ use Illuminate\Http\Request;
|
||||
|
||||
Route::middleware('auth:api')->get('/otm', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
|
|
||||
*/
|
||||
|
||||
Route::prefix('otm')->group(function() {
|
||||
Route::prefix('otm')->group(function () {
|
||||
Route::get('/', 'OTMController@index');
|
||||
});
|
||||
|
||||
@@ -4,10 +4,11 @@ namespace Modules\OTM\Support;
|
||||
|
||||
class ModuleInfo
|
||||
{
|
||||
protected static string $moduleName = 'OTM';
|
||||
protected static string $moduleName = 'OTM';
|
||||
|
||||
/**
|
||||
* defines the Module Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function name(): string
|
||||
@@ -17,6 +18,7 @@ class ModuleInfo
|
||||
|
||||
/**
|
||||
* define the lowercase version
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function nameLower(): string
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<h2 class="text-lg font-semibold text-slate-950 dark:text-white">Recent companies</h2>
|
||||
<div class="mt-4 overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm dark:divide-slate-800">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead
|
||||
class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500 dark:bg-slate-900 dark:text-slate-400">
|
||||
<tr>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<div
|
||||
class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<table class="min-w-full text-sm">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="p-4 text-left">Company</th>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<div
|
||||
class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<table class="min-w-full text-sm">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="p-4 text-left">Company</th>
|
||||
|
||||
@@ -63,9 +63,7 @@ class PricingServiceProvider extends ServiceProvider
|
||||
$commandPath = __DIR__ . '/../Console';
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(function ($file) {
|
||||
$class = 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
|
||||
return $class;
|
||||
return 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
|
||||
})
|
||||
->filter(function ($class) {
|
||||
return class_exists($class);
|
||||
|
||||
@@ -14,17 +14,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $moduleNamespace = 'Modules\Pricing\Http\Controllers';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Report</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Area</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Shipment</th>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Trailer</th>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<div
|
||||
class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<table class="min-w-full text-sm">
|
||||
<caption class="sr-only">Data table</caption>
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="p-4 text-left">Warehouse</th>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Exceptions\DatabaseStageException;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class DatabaseStage extends Command
|
||||
{
|
||||
@@ -16,396 +16,280 @@ class DatabaseStage extends Command
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$local = [
|
||||
'host' => config('database.connections.pgsql.host'),
|
||||
'port' => config('database.connections.pgsql.port'),
|
||||
'database' => config('database.connections.pgsql.database'),
|
||||
'username' => config('database.connections.pgsql.username'),
|
||||
'password' => config('database.connections.pgsql.password'),
|
||||
];
|
||||
$local = $this->localConfiguration();
|
||||
$stage = $this->stageConfiguration();
|
||||
|
||||
$stage = [
|
||||
'ssh_host' => env('STAGE_SSH_HOST'),
|
||||
'ssh_user' => env('STAGE_SSH_USER'),
|
||||
'ssh_port' => env('STAGE_SSH_PORT', 22),
|
||||
|
||||
'db_host' => env('STAGE_DB_HOST', '127.0.0.1'),
|
||||
'db_port' => env('STAGE_DB_PORT', 5432),
|
||||
'database' => env('STAGE_DB_DATABASE'),
|
||||
'username' => env('STAGE_DB_USERNAME'),
|
||||
'password' => env('STAGE_DB_PASSWORD'),
|
||||
|
||||
// PostgreSQL administrator used to recreate the stage DB.
|
||||
'admin_username' => env('STAGE_DB_ADMIN_USERNAME', 'postgres'),
|
||||
];
|
||||
|
||||
foreach ([
|
||||
'STAGE_SSH_HOST' => $stage['ssh_host'],
|
||||
'STAGE_SSH_USER' => $stage['ssh_user'],
|
||||
'STAGE_DB_DATABASE' => $stage['database'],
|
||||
'STAGE_DB_USERNAME' => $stage['username'],
|
||||
] as $name => $value) {
|
||||
if (! $value) {
|
||||
$this->error("Missing {$name}.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
if (! $this->validateStageConfiguration($stage)) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$timestamp = now()->format('Ymd-His');
|
||||
$filename = "wms-{$timestamp}.dump";
|
||||
[$localDump, $remoteDump] = $this->dumpPaths();
|
||||
$this->displayCloneSummary($local, $stage);
|
||||
|
||||
$localDump = storage_path("app/{$filename}");
|
||||
$remoteDump = "/tmp/{$filename}";
|
||||
|
||||
$this->newLine();
|
||||
$this->components->info('WMS PostgreSQL FULL CLONE → Stage');
|
||||
$this->newLine();
|
||||
|
||||
$this->table(
|
||||
['Setting', 'Value'],
|
||||
[
|
||||
['Local database', $local['database']],
|
||||
['Stage server', "{$stage['ssh_user']}@{$stage['ssh_host']}"],
|
||||
['Stage database', $stage['database']],
|
||||
['Stage owner', $stage['username']],
|
||||
]
|
||||
);
|
||||
|
||||
if (
|
||||
! $this->option('force') &&
|
||||
! $this->confirm(
|
||||
"This will DROP and completely RECREATE '{$stage['database']}'. Continue?",
|
||||
false
|
||||
)
|
||||
) {
|
||||
if (! $this->confirmed($stage)) {
|
||||
$this->warn('Cancelled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Ensure local dump directory exists
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$directory = dirname($localDump);
|
||||
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Dump COMPLETE local database
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$this->components->task(
|
||||
'Backing up complete local PostgreSQL database',
|
||||
function () use ($local, $localDump) {
|
||||
$result = Process::env([
|
||||
'PGPASSWORD' => $local['password'],
|
||||
])->timeout(3600)->run([
|
||||
'pg_dump',
|
||||
'--host='.$local['host'],
|
||||
'--port='.$local['port'],
|
||||
'--username='.$local['username'],
|
||||
|
||||
// Custom archive format.
|
||||
'--format=custom',
|
||||
|
||||
// Maximum compression.
|
||||
'--compress=9',
|
||||
|
||||
// Stage user becomes owner.
|
||||
'--no-owner',
|
||||
|
||||
// Do not copy local ACLs/grants.
|
||||
'--no-acl',
|
||||
|
||||
// Include everything in the database.
|
||||
'--file='.$localDump,
|
||||
|
||||
$local['database'],
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException(
|
||||
"pg_dump failed:\n".$result->errorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
if (! file_exists($localDump) || filesize($localDump) === 0) {
|
||||
throw new RuntimeException('Database dump was not created or is empty.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
$dumpSize = $this->formatBytes(filesize($localDump));
|
||||
|
||||
$this->line(" Dump size: {$dumpSize}");
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Inspect dump
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$this->components->task(
|
||||
'Verifying PostgreSQL archive',
|
||||
function () use ($localDump) {
|
||||
$result = Process::run([
|
||||
'pg_restore',
|
||||
'--list',
|
||||
$localDump,
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Invalid PostgreSQL archive:\n".$result->errorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Upload
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$this->components->task(
|
||||
'Uploading database backup',
|
||||
function () use ($stage, $localDump, $remoteDump) {
|
||||
$result = Process::timeout(3600)->run([
|
||||
'scp',
|
||||
'-P',
|
||||
(string) $stage['ssh_port'],
|
||||
$localDump,
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}:{$remoteDump}",
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Upload failed:\n".$result->errorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* DROP + recreate stage database
|
||||
*
|
||||
* Pegasus permits the SSH user to run PostgreSQL commands
|
||||
* through sudo -u postgres.
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$this->components->task(
|
||||
'Recreating stage database',
|
||||
function () use ($stage) {
|
||||
$database = $this->quoteSqlIdentifier($stage['database']);
|
||||
$owner = $this->quoteSqlIdentifier($stage['username']);
|
||||
|
||||
$terminateDatabase = str_replace(
|
||||
"'",
|
||||
"''",
|
||||
$stage['database']
|
||||
);
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT pg_terminate_backend(pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = '{$terminateDatabase}'
|
||||
AND pid <> pg_backend_pid();
|
||||
|
||||
DROP DATABASE IF EXISTS {$database};
|
||||
CREATE DATABASE {$database}
|
||||
WITH OWNER {$owner}
|
||||
ENCODING 'UTF8'
|
||||
TEMPLATE template0;
|
||||
SQL;
|
||||
|
||||
$remoteCommand = sprintf(
|
||||
'sudo -u %s psql --set ON_ERROR_STOP=on --dbname=postgres',
|
||||
escapeshellarg($stage['admin_username'])
|
||||
);
|
||||
|
||||
$result = Process::input($sql)
|
||||
->timeout(300)
|
||||
->run([
|
||||
'ssh',
|
||||
'-p',
|
||||
(string) $stage['ssh_port'],
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||
$remoteCommand,
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Unable to recreate stage database:\n".
|
||||
$result->errorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Restore COMPLETE archive
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$this->components->task(
|
||||
'Restoring complete stage database',
|
||||
function () use ($stage, $remoteDump) {
|
||||
$restore = sprintf(
|
||||
'PGPASSWORD=%s pg_restore '.
|
||||
'--host=%s '.
|
||||
'--port=%s '.
|
||||
'--username=%s '.
|
||||
'--dbname=%s '.
|
||||
'--no-owner '.
|
||||
'--no-acl '.
|
||||
'--exit-on-error '.
|
||||
'--verbose '.
|
||||
'%s',
|
||||
escapeshellarg($stage['password']),
|
||||
escapeshellarg($stage['db_host']),
|
||||
escapeshellarg((string) $stage['db_port']),
|
||||
escapeshellarg($stage['username']),
|
||||
escapeshellarg($stage['database']),
|
||||
escapeshellarg($remoteDump),
|
||||
);
|
||||
|
||||
$result = Process::timeout(3600)->run([
|
||||
'ssh',
|
||||
'-p',
|
||||
(string) $stage['ssh_port'],
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||
$restore,
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Database restore failed:\n".
|
||||
$result->errorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Verify stage tables
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
$this->components->task(
|
||||
'Verifying restored database',
|
||||
function () use ($stage) {
|
||||
$command = sprintf(
|
||||
'PGPASSWORD=%s psql '.
|
||||
'--host=%s '.
|
||||
'--port=%s '.
|
||||
'--username=%s '.
|
||||
'--dbname=%s '.
|
||||
'--tuples-only '.
|
||||
'--command=%s',
|
||||
escapeshellarg($stage['password']),
|
||||
escapeshellarg($stage['db_host']),
|
||||
escapeshellarg((string) $stage['db_port']),
|
||||
escapeshellarg($stage['username']),
|
||||
escapeshellarg($stage['database']),
|
||||
escapeshellarg(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';"
|
||||
),
|
||||
);
|
||||
|
||||
$result = Process::run([
|
||||
'ssh',
|
||||
'-p',
|
||||
(string) $stage['ssh_port'],
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||
$command,
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Database verification failed:\n".
|
||||
$result->errorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
$count = trim($result->output());
|
||||
|
||||
$this->line(" Stage public tables: {$count}");
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------
|
||||
* Cleanup
|
||||
* ---------------------------------------------------------
|
||||
*/
|
||||
|
||||
Process::run([
|
||||
'ssh',
|
||||
'-p',
|
||||
(string) $stage['ssh_port'],
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}",
|
||||
'rm',
|
||||
'-f',
|
||||
$remoteDump,
|
||||
]);
|
||||
|
||||
if (! $this->option('keep')) {
|
||||
@unlink($localDump);
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->components->info(
|
||||
'Stage PostgreSQL database cloned successfully.'
|
||||
);
|
||||
|
||||
$this->line(
|
||||
"Local {$local['database']} → Stage {$stage['database']}"
|
||||
);
|
||||
|
||||
return self::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->newLine();
|
||||
$this->components->error('Database clone failed.');
|
||||
$this->error($e->getMessage());
|
||||
|
||||
$this->newLine();
|
||||
$this->warn(
|
||||
"The dump may still exist at {$localDump} and/or {$remoteDump}."
|
||||
);
|
||||
$this->runClone($local, $stage, $localDump, $remoteDump);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->reportFailure($exception, $localDump, $remoteDump);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->reportSuccess($local, $stage);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function localConfiguration(): array
|
||||
{
|
||||
return [
|
||||
'host' => config('database.connections.pgsql.host'),
|
||||
'port' => config('database.connections.pgsql.port'),
|
||||
'database' => config('database.connections.pgsql.database'),
|
||||
'username' => config('database.connections.pgsql.username'),
|
||||
'password' => config('database.connections.pgsql.password'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function stageConfiguration(): array
|
||||
{
|
||||
return [
|
||||
'ssh_host' => env('STAGE_SSH_HOST'),
|
||||
'ssh_user' => env('STAGE_SSH_USER'),
|
||||
'ssh_port' => env('STAGE_SSH_PORT', 22),
|
||||
'db_host' => env('STAGE_DB_HOST', '127.0.0.1'),
|
||||
'db_port' => env('STAGE_DB_PORT', 5432),
|
||||
'database' => env('STAGE_DB_DATABASE'),
|
||||
'username' => env('STAGE_DB_USERNAME'),
|
||||
'password' => env('STAGE_DB_PASSWORD'),
|
||||
'admin_username' => env('STAGE_DB_ADMIN_USERNAME', 'postgres'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function validateStageConfiguration(array $stage): bool
|
||||
{
|
||||
$required = [
|
||||
'STAGE_SSH_HOST' => $stage['ssh_host'],
|
||||
'STAGE_SSH_USER' => $stage['ssh_user'],
|
||||
'STAGE_DB_DATABASE' => $stage['database'],
|
||||
'STAGE_DB_USERNAME' => $stage['username'],
|
||||
];
|
||||
|
||||
foreach ($required as $name => $value) {
|
||||
if (! $value) {
|
||||
$this->error("Missing {$name}.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array{string, string} */
|
||||
private function dumpPaths(): array
|
||||
{
|
||||
$filename = 'wms-' . now()->format('Ymd-His') . '.dump';
|
||||
|
||||
return [storage_path("app/{$filename}"), "/tmp/{$filename}"];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local @param array<string, mixed> $stage */
|
||||
private function displayCloneSummary(array $local, array $stage): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->components->info('WMS PostgreSQL FULL CLONE → Stage');
|
||||
$this->newLine();
|
||||
$this->table(['Setting', 'Value'], [
|
||||
['Local database', $local['database']],
|
||||
['Stage server', "{$stage['ssh_user']}@{$stage['ssh_host']}"],
|
||||
['Stage database', $stage['database']],
|
||||
['Stage owner', $stage['username']],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function confirmed(array $stage): bool
|
||||
{
|
||||
return $this->option('force') || $this->confirm(
|
||||
"This will DROP and completely RECREATE '{$stage['database']}'. Continue?",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local @param array<string, mixed> $stage */
|
||||
private function runClone(array $local, array $stage, string $localDump, string $remoteDump): void
|
||||
{
|
||||
$this->ensureDumpDirectory($localDump);
|
||||
$this->createDump($local, $localDump);
|
||||
$this->line(' Dump size: ' . $this->formatBytes((int) filesize($localDump)));
|
||||
$this->verifyDump($localDump);
|
||||
$this->uploadDump($stage, $localDump, $remoteDump);
|
||||
$this->recreateStageDatabase($stage);
|
||||
$this->restoreStageDatabase($stage, $remoteDump);
|
||||
$this->verifyStageDatabase($stage);
|
||||
$this->cleanupDumps($stage, $localDump, $remoteDump);
|
||||
}
|
||||
|
||||
private function ensureDumpDirectory(string $localDump): void
|
||||
{
|
||||
$directory = dirname($localDump);
|
||||
|
||||
if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
|
||||
throw new DatabaseStageException("Unable to create dump directory: {$directory}");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local */
|
||||
private function createDump(array $local, string $localDump): void
|
||||
{
|
||||
$this->components->task('Backing up complete local PostgreSQL database', function () use ($local, $localDump): bool {
|
||||
$result = Process::env(['PGPASSWORD' => $local['password']])->timeout(3600)->run([
|
||||
'pg_dump', '--host=' . $local['host'], '--port=' . $local['port'],
|
||||
'--username=' . $local['username'], '--format=custom', '--compress=9',
|
||||
'--no-owner', '--no-acl', '--file=' . $localDump, $local['database'],
|
||||
]);
|
||||
$this->throwWhenFailed($result->failed(), "pg_dump failed:\n" . $result->errorOutput());
|
||||
|
||||
if (! file_exists($localDump) || filesize($localDump) === 0) {
|
||||
throw new DatabaseStageException('Database dump was not created or is empty.');
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private function verifyDump(string $localDump): void
|
||||
{
|
||||
$this->components->task('Verifying PostgreSQL archive', function () use ($localDump): bool {
|
||||
$result = Process::run(['pg_restore', '--list', $localDump]);
|
||||
$this->throwWhenFailed($result->failed(), "Invalid PostgreSQL archive:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function uploadDump(array $stage, string $localDump, string $remoteDump): void
|
||||
{
|
||||
$this->components->task('Uploading database backup', function () use ($stage, $localDump, $remoteDump): bool {
|
||||
$result = Process::timeout(3600)->run([
|
||||
'scp', '-P', (string) $stage['ssh_port'], $localDump,
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}:{$remoteDump}",
|
||||
]);
|
||||
$this->throwWhenFailed($result->failed(), "Upload failed:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function recreateStageDatabase(array $stage): void
|
||||
{
|
||||
$this->components->task('Recreating stage database', function () use ($stage): bool {
|
||||
$database = $this->quoteSqlIdentifier((string) $stage['database']);
|
||||
$owner = $this->quoteSqlIdentifier((string) $stage['username']);
|
||||
$terminateDatabase = str_replace("'", "''", (string) $stage['database']);
|
||||
$sql = "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{$terminateDatabase}' AND pid <> pg_backend_pid();\n"
|
||||
. "DROP DATABASE IF EXISTS {$database};\nCREATE DATABASE {$database} WITH OWNER {$owner} ENCODING 'UTF8' TEMPLATE template0;";
|
||||
$remoteCommand = sprintf(
|
||||
'sudo -u %s psql --set ON_ERROR_STOP=on --dbname=postgres',
|
||||
escapeshellarg((string) $stage['admin_username']),
|
||||
);
|
||||
$result = Process::input($sql)->timeout(300)->run($this->sshCommand($stage, $remoteCommand));
|
||||
$this->throwWhenFailed($result->failed(), "Unable to recreate stage database:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function restoreStageDatabase(array $stage, string $remoteDump): void
|
||||
{
|
||||
$this->components->task('Restoring complete stage database', function () use ($stage, $remoteDump): bool {
|
||||
$restore = sprintf(
|
||||
'PGPASSWORD=%s pg_restore --host=%s --port=%s --username=%s --dbname=%s --no-owner --no-acl --exit-on-error --verbose %s',
|
||||
escapeshellarg((string) $stage['password']), escapeshellarg((string) $stage['db_host']),
|
||||
escapeshellarg((string) $stage['db_port']), escapeshellarg((string) $stage['username']),
|
||||
escapeshellarg((string) $stage['database']), escapeshellarg($remoteDump),
|
||||
);
|
||||
$result = Process::timeout(3600)->run($this->sshCommand($stage, $restore));
|
||||
$this->throwWhenFailed($result->failed(), "Database restore failed:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function verifyStageDatabase(array $stage): void
|
||||
{
|
||||
$this->components->task('Verifying restored database', function () use ($stage): bool {
|
||||
$command = sprintf(
|
||||
'PGPASSWORD=%s psql --host=%s --port=%s --username=%s --dbname=%s --tuples-only --command=%s',
|
||||
escapeshellarg((string) $stage['password']), escapeshellarg((string) $stage['db_host']),
|
||||
escapeshellarg((string) $stage['db_port']), escapeshellarg((string) $stage['username']),
|
||||
escapeshellarg((string) $stage['database']),
|
||||
escapeshellarg("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';"),
|
||||
);
|
||||
$result = Process::run($this->sshCommand($stage, $command));
|
||||
$this->throwWhenFailed($result->failed(), "Database verification failed:\n" . $result->errorOutput());
|
||||
$this->line(' Stage public tables: ' . trim($result->output()));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function cleanupDumps(array $stage, string $localDump, string $remoteDump): void
|
||||
{
|
||||
Process::run($this->sshCommand($stage, 'rm -f ' . escapeshellarg($remoteDump)));
|
||||
|
||||
if (! $this->option('keep')) {
|
||||
@unlink($localDump);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage @return list<string> */
|
||||
private function sshCommand(array $stage, string $command): array
|
||||
{
|
||||
return ['ssh', '-p', (string) $stage['ssh_port'], "{$stage['ssh_user']}@{$stage['ssh_host']}", $command];
|
||||
}
|
||||
|
||||
private function throwWhenFailed(bool $failed, string $message): void
|
||||
{
|
||||
if ($failed) {
|
||||
throw new DatabaseStageException($message);
|
||||
}
|
||||
}
|
||||
|
||||
private function reportFailure(\Throwable $exception, string $localDump, string $remoteDump): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->components->error('Database clone failed.');
|
||||
$this->error($exception->getMessage());
|
||||
$this->newLine();
|
||||
$this->warn("The dump may still exist at {$localDump} and/or {$remoteDump}.");
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local @param array<string, mixed> $stage */
|
||||
private function reportSuccess(array $local, array $stage): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->components->info('Stage PostgreSQL database cloned successfully.');
|
||||
$this->line("Local {$local['database']} → Stage {$stage['database']}");
|
||||
}
|
||||
|
||||
private function quoteSqlIdentifier(string $identifier): string
|
||||
{
|
||||
return '"'.str_replace('"', '""', $identifier).'"';
|
||||
return '"' . str_replace('"', '""', $identifier) . '"';
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
@@ -418,8 +302,8 @@ SQL;
|
||||
: 0;
|
||||
|
||||
return number_format(
|
||||
$bytes / (1024 ** $power),
|
||||
$power === 0 ? 0 : 2
|
||||
).' '.$units[$power];
|
||||
$bytes / (1024 ** $power),
|
||||
$power === 0 ? 0 : 2,
|
||||
) . ' ' . $units[$power];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class DatabaseStageException extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -3,22 +3,10 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Horizon;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Horizon gate.
|
||||
|
||||
@@ -95,7 +95,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
// No container bindings are required; tenancy is configured through event listeners in boot().
|
||||
}
|
||||
|
||||
public function boot()
|
||||
|
||||
+30
-26
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Deployer;
|
||||
|
||||
require 'recipe/laravel.php';
|
||||
require_once 'recipe/laravel.php';
|
||||
|
||||
set('writable_use_sudo', true);
|
||||
set('clear_use_sudo', true);
|
||||
@@ -12,6 +12,10 @@ set('repository', 'git@git.absdev.net:mag/WMS.git');
|
||||
set('node_version', '22.12.0');
|
||||
set('allow_anonymous_stats', false);
|
||||
|
||||
const RELEASE_CD = 'cd {{release_path}} && ';
|
||||
const RELEASE_AND_CD = ' && cd {{release_path}}';
|
||||
const DEPLOY_SYMLINK = 'deploy:symlink';
|
||||
|
||||
add('shared_files', ['.env']);
|
||||
add('shared_dirs', ['storage']);
|
||||
add('writable_dirs', ['storage', 'bootstrap/cache']);
|
||||
@@ -41,59 +45,59 @@ host('stage')
|
||||
*/
|
||||
task('build', function (): void {
|
||||
$node = 'export NVM_DIR="$HOME/.nvm"'
|
||||
.' && [ -s "$NVM_DIR/nvm.sh" ]'
|
||||
.' && . "$NVM_DIR/nvm.sh"'
|
||||
.' && nvm use {{node_version}}';
|
||||
. ' && [ -s "$NVM_DIR/nvm.sh" ]'
|
||||
. ' && . "$NVM_DIR/nvm.sh"'
|
||||
. ' && nvm use {{node_version}}';
|
||||
|
||||
run(
|
||||
$node
|
||||
.' && cd {{release_path}}'
|
||||
.' && npm ci --no-audit --no-fund'
|
||||
. RELEASE_AND_CD
|
||||
. ' && npm ci --no-audit --no-fund',
|
||||
);
|
||||
|
||||
run(
|
||||
$node
|
||||
.' && cd {{release_path}}'
|
||||
.' && for lock in Modules/*/package-lock.json; do'
|
||||
.' [ -f "$lock" ] || continue;'
|
||||
.' module_dir="$(dirname "$lock")";'
|
||||
.' echo "[Modules] npm ci: $module_dir";'
|
||||
.' (cd "$module_dir" && npm ci --no-audit --no-fund);'
|
||||
.' done'
|
||||
. RELEASE_AND_CD
|
||||
. ' && for lock in Modules/*/package-lock.json; do'
|
||||
. ' [ -f "$lock" ] || continue;'
|
||||
. ' module_dir="$(dirname "$lock")";'
|
||||
. ' echo "[Modules] npm ci: $module_dir";'
|
||||
. ' (cd "$module_dir" && npm ci --no-audit --no-fund);'
|
||||
. ' done',
|
||||
);
|
||||
|
||||
run(
|
||||
$node
|
||||
.' && cd {{release_path}}'
|
||||
.' && npm run build'
|
||||
. RELEASE_AND_CD
|
||||
. ' && npm run build',
|
||||
);
|
||||
});
|
||||
|
||||
task('queue:restart', function (): void {
|
||||
run('cd {{release_path}} && php artisan queue:restart');
|
||||
run(RELEASE_CD . 'php artisan queue:restart');
|
||||
});
|
||||
|
||||
task('deploy:write_revision_file', function (): void {
|
||||
run('cd {{release_path}} && echo "{{branch}}" > BRANCH');
|
||||
run('cd {{release_path}} && date -u +"%Y-%m-%dT%H:%M:%SZ" > DEPLOYED_AT');
|
||||
run(RELEASE_CD . 'echo "{{branch}}" > BRANCH');
|
||||
run(RELEASE_CD . 'date -u +"%Y-%m-%dT%H:%M:%SZ" > DEPLOYED_AT');
|
||||
});
|
||||
|
||||
task('artisan:clear', function (): void {
|
||||
run('cd {{release_path}} && php artisan view:clear');
|
||||
run('cd {{release_path}} && php artisan optimize:clear');
|
||||
run('cd {{release_path}} && php artisan config:clear');
|
||||
run('cd {{release_path}} && php artisan route:clear');
|
||||
run(RELEASE_CD . 'php artisan view:clear');
|
||||
run(RELEASE_CD . 'php artisan optimize:clear');
|
||||
run(RELEASE_CD . 'php artisan config:clear');
|
||||
run(RELEASE_CD . 'php artisan route:clear');
|
||||
});
|
||||
|
||||
task('services:restart', function (): void {
|
||||
run('sudo -n nginx -t');
|
||||
run('sudo -n systemctl reload nginx');
|
||||
run('sudo -n systemctl restart php8.3-fpm');
|
||||
run('sudo -n systemctl restart php8.4-fpm');
|
||||
});
|
||||
|
||||
after('deploy:update_code', 'deploy:write_revision_file');
|
||||
after('deploy:vendors', 'build');
|
||||
after('deploy:symlink', 'artisan:clear');
|
||||
after('deploy:symlink', 'queue:restart');
|
||||
after(DEPLOY_SYMLINK, 'artisan:clear');
|
||||
after(DEPLOY_SYMLINK, 'queue:restart');
|
||||
after('deploy:failed', 'deploy:unlock');
|
||||
after('deploy:symlink', 'services:restart');
|
||||
after(DEPLOY_SYMLINK, 'services:restart');
|
||||
|
||||
Generated
+4771
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
function cssVar(name, fallback) {
|
||||
var value = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
let value = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return value && value.trim() !== '' ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
}
|
||||
|
||||
function enrichDataset(dataset, index, type) {
|
||||
var colours = palette();
|
||||
var colour = colours[index % colours.length];
|
||||
let colours = palette();
|
||||
let colour = colours[index % colours.length];
|
||||
|
||||
if (type === 'doughnut' || type === 'pie') {
|
||||
dataset.backgroundColor = colours;
|
||||
@@ -39,10 +39,10 @@
|
||||
}
|
||||
|
||||
function optionsFor(type) {
|
||||
var textColour = cssVar('--wms-chart-text', '#334155');
|
||||
var gridColour = cssVar('--wms-chart-grid', 'rgba(148, 163, 184, 0.25)');
|
||||
let textColour = cssVar('--wms-chart-text', '#334155');
|
||||
let gridColour = cssVar('--wms-chart-grid', 'rgba(148, 163, 184, 0.25)');
|
||||
|
||||
var options = {
|
||||
let options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
@@ -100,7 +100,7 @@
|
||||
}
|
||||
|
||||
function normaliseConfig(rawConfig, type) {
|
||||
var config = rawConfig || {};
|
||||
let config = rawConfig || {};
|
||||
config.labels = config.labels || [];
|
||||
config.datasets = (config.datasets || []).map(function (dataset, index) {
|
||||
return enrichDataset(dataset, index, type);
|
||||
@@ -115,9 +115,9 @@
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-wms-chart]').forEach(function (canvas) {
|
||||
var type = canvas.getAttribute('data-chart-type') || 'line';
|
||||
var raw = canvas.getAttribute('data-chart-config') || '{}';
|
||||
var config = {};
|
||||
let type = canvas.getAttribute('data-chart-type') || 'line';
|
||||
let raw = canvas.getAttribute('data-chart-config') || '{}';
|
||||
let config = {};
|
||||
|
||||
try {
|
||||
config = JSON.parse(raw);
|
||||
@@ -125,7 +125,7 @@
|
||||
config = { labels: [], datasets: [] };
|
||||
}
|
||||
|
||||
new Chart(canvas, {
|
||||
canvas.wmsChart = new Chart(canvas, {
|
||||
type: type,
|
||||
data: normaliseConfig(config, type),
|
||||
options: optionsFor(type)
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ function renderWmsCharts() {
|
||||
},
|
||||
];
|
||||
|
||||
new Chart(canvas, {
|
||||
canvas.wmsChart = new Chart(canvas, {
|
||||
type,
|
||||
data: {
|
||||
labels,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
function cssVar(name, fallback) {
|
||||
var value = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
let value = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return value && value.trim() !== '' ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
}
|
||||
|
||||
function enrichDataset(dataset, index, type) {
|
||||
var colours = palette();
|
||||
var colour = colours[index % colours.length];
|
||||
let colours = palette();
|
||||
let colour = colours[index % colours.length];
|
||||
|
||||
if (type === 'doughnut' || type === 'pie') {
|
||||
dataset.backgroundColor = colours;
|
||||
@@ -39,10 +39,10 @@
|
||||
}
|
||||
|
||||
function optionsFor(type) {
|
||||
var textColour = cssVar('--wms-chart-text', '#334155');
|
||||
var gridColour = cssVar('--wms-chart-grid', 'rgba(148, 163, 184, 0.25)');
|
||||
let textColour = cssVar('--wms-chart-text', '#334155');
|
||||
let gridColour = cssVar('--wms-chart-grid', 'rgba(148, 163, 184, 0.25)');
|
||||
|
||||
var options = {
|
||||
let options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
@@ -100,7 +100,7 @@
|
||||
}
|
||||
|
||||
function normaliseConfig(rawConfig, type) {
|
||||
var config = rawConfig || {};
|
||||
let config = rawConfig || {};
|
||||
config.labels = config.labels || [];
|
||||
config.datasets = (config.datasets || []).map(function (dataset, index) {
|
||||
return enrichDataset(dataset, index, type);
|
||||
@@ -115,9 +115,9 @@
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-wms-chart]').forEach(function (canvas) {
|
||||
var type = canvas.getAttribute('data-chart-type') || 'line';
|
||||
var raw = canvas.getAttribute('data-chart-config') || '{}';
|
||||
var config = {};
|
||||
let type = canvas.getAttribute('data-chart-type') || 'line';
|
||||
let raw = canvas.getAttribute('data-chart-config') || '{}';
|
||||
let config = {};
|
||||
|
||||
try {
|
||||
config = JSON.parse(raw);
|
||||
@@ -125,7 +125,7 @@
|
||||
config = { labels: [], datasets: [] };
|
||||
}
|
||||
|
||||
new Chart(canvas, {
|
||||
canvas.wmsChart = new Chart(canvas, {
|
||||
type: type,
|
||||
data: normaliseConfig(config, type),
|
||||
options: optionsFor(type)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace, monospace;
|
||||
font-family: monospace;
|
||||
font-size: 1em
|
||||
}
|
||||
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SONAR_URL="http://192.168.1.2:9001"
|
||||
PROJECT="Warehouse-Management-System"
|
||||
OUTPUT="sonarqube-issues.csv"
|
||||
TMP=$(mktemp)
|
||||
|
||||
if [ -z "${SONAR_TOKEN:-}" ]; then
|
||||
echo "SONAR_TOKEN is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Downloading SonarQube issues for $PROJECT..."
|
||||
|
||||
echo '[]' > "$TMP"
|
||||
|
||||
PAGE=1
|
||||
PAGE_SIZE=500
|
||||
|
||||
while true; do
|
||||
echo "Fetching page $PAGE..."
|
||||
|
||||
RESPONSE=$(curl --connect-timeout 5 --max-time 60 -sS \
|
||||
-u "$SONAR_TOKEN:" \
|
||||
"$SONAR_URL/api/issues/search?componentKeys=$PROJECT&ps=$PAGE_SIZE&p=$PAGE")
|
||||
|
||||
COUNT=$(echo "$RESPONSE" | jq '.issues | length')
|
||||
TOTAL=$(echo "$RESPONSE" | jq '.total')
|
||||
|
||||
echo " $COUNT issues received ($TOTAL total)"
|
||||
|
||||
jq -s '.[0] + .[1].issues' "$TMP" <(echo "$RESPONSE") > "$TMP.new"
|
||||
mv "$TMP.new" "$TMP"
|
||||
|
||||
[ "$COUNT" -lt "$PAGE_SIZE" ] && break
|
||||
|
||||
PAGE=$((PAGE + 1))
|
||||
done
|
||||
|
||||
jq -r '
|
||||
[
|
||||
"Severity",
|
||||
"Type",
|
||||
"Status",
|
||||
"Rule",
|
||||
"File",
|
||||
"Line",
|
||||
"Message",
|
||||
"Effort",
|
||||
"Assignee",
|
||||
"Author",
|
||||
"Created",
|
||||
"Updated"
|
||||
],
|
||||
(.[] | [
|
||||
.severity,
|
||||
.type,
|
||||
.status,
|
||||
.rule,
|
||||
(.component | sub("^Warehouse-Management-System:";"")),
|
||||
(.line // ""),
|
||||
.message,
|
||||
(.effort // ""),
|
||||
(.assignee // ""),
|
||||
(.author // ""),
|
||||
.creationDate,
|
||||
.updateDate
|
||||
]) | @csv
|
||||
' "$TMP" > "$OUTPUT"
|
||||
|
||||
rm "$TMP"
|
||||
|
||||
echo
|
||||
echo "Complete."
|
||||
echo "Report: $OUTPUT"
|
||||
echo "Issues exported: $(($(wc -l < "$OUTPUT") - 1))"
|
||||
@@ -0,0 +1,69 @@
|
||||
sonar.projectKey=Warehouse-Management-System
|
||||
sonar.projectName=Warehouse Management System
|
||||
sonar.sources=.
|
||||
sonar.exclusions=vendor/**,node_modules/**,storage/**,bootstrap/cache/**,public/build/**,__MACOSX/**
|
||||
|
||||
# Project conventions/framework patterns that are not defects.
|
||||
# S103: line length is handled by the project's formatter/review conventions.
|
||||
# S1808: formatting/alignment is handled by Pint.
|
||||
# S116: snake_case Livewire public properties intentionally mirror form/database fields.
|
||||
# S4833: Deployer/Laravel module bootstraps intentionally load recipe/route files.
|
||||
# Web title checks are not valid on Blade/Vue partials/components which are not standalone HTML documents.
|
||||
sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6,e7,e8,e9,e10,e11,e12,e13,e14,e15,e16,e17,e18,e19,e20
|
||||
sonar.issue.ignore.multicriteria.e1.ruleKey=php:S103
|
||||
sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.php
|
||||
sonar.issue.ignore.multicriteria.e2.ruleKey=php:S1808
|
||||
sonar.issue.ignore.multicriteria.e2.resourceKey=**/*.php
|
||||
sonar.issue.ignore.multicriteria.e3.ruleKey=php:S116
|
||||
sonar.issue.ignore.multicriteria.e3.resourceKey=Modules/Helpdesk/**/*.php
|
||||
sonar.issue.ignore.multicriteria.e4.ruleKey=php:S4833
|
||||
sonar.issue.ignore.multicriteria.e4.resourceKey=deploy.php
|
||||
sonar.issue.ignore.multicriteria.e5.ruleKey=php:S4833
|
||||
sonar.issue.ignore.multicriteria.e5.resourceKey=Modules/*/Providers/*ServiceProvider.php
|
||||
sonar.issue.ignore.multicriteria.e6.ruleKey=Web:PageWithoutTitleCheck
|
||||
sonar.issue.ignore.multicriteria.e6.resourceKey=**/*.{blade.php,vue}
|
||||
|
||||
# Sonar CSS does not parse Tailwind v4 directives used by the application stylesheet.
|
||||
sonar.issue.ignore.multicriteria.e7.ruleKey=css:S4662
|
||||
sonar.issue.ignore.multicriteria.e7.resourceKey=resources/css/app.css
|
||||
# Empty module Sass entrypoints are intentional placeholders for module-owned assets.
|
||||
sonar.issue.ignore.multicriteria.e8.ruleKey=css:S4667
|
||||
sonar.issue.ignore.multicriteria.e8.resourceKey=Modules/*/Resources/assets/sass/app.scss
|
||||
|
||||
# Laravel global helper functions intentionally use snake_case to match framework conventions.
|
||||
sonar.issue.ignore.multicriteria.e9.ruleKey=php:S100
|
||||
sonar.issue.ignore.multicriteria.e9.resourceKey=Modules/*/Helpers/Helper.php
|
||||
# Framework bootstrap/route files conventionally use require and are intentionally not converted to require_once.
|
||||
sonar.issue.ignore.multicriteria.e10.ruleKey=php:S2003
|
||||
sonar.issue.ignore.multicriteria.e10.resourceKey=**/{public/index.php,routes/*.php,deploy.php,tests/CreatesApplication.php}
|
||||
|
||||
# Vue pages and Blade layout fragments receive their document title from the application layout/head component.
|
||||
sonar.issue.ignore.multicriteria.e11.ruleKey=Web:PageWithoutTitleCheck
|
||||
sonar.issue.ignore.multicriteria.e11.resourceKey=**/*.vue
|
||||
sonar.issue.ignore.multicriteria.e12.ruleKey=Web:PageWithoutTitleCheck
|
||||
sonar.issue.ignore.multicriteria.e12.resourceKey=**/*.blade.php
|
||||
|
||||
# Laravel's generated welcome/error templates contain browser/vendor CSS that intentionally repeats selectors.
|
||||
sonar.issue.ignore.multicriteria.e13.ruleKey=css:S4666
|
||||
sonar.issue.ignore.multicriteria.e13.resourceKey=resources/views/welcome.blade.php
|
||||
sonar.issue.ignore.multicriteria.e14.ruleKey=css:S4662
|
||||
sonar.issue.ignore.multicriteria.e14.resourceKey=resources/views/welcome.blade.php
|
||||
|
||||
# Helpdesk CSS deliberately layers later overrides over base component/select2 rules.
|
||||
sonar.issue.ignore.multicriteria.e15.ruleKey=css:S4666
|
||||
sonar.issue.ignore.multicriteria.e15.resourceKey=Modules/Helpdesk/Resources/assets/css/app.css
|
||||
|
||||
# Repeated literals in tests are fixtures/assertion vocabulary; constants would make the tests less readable.
|
||||
sonar.issue.ignore.multicriteria.e16.ruleKey=php:S1192
|
||||
sonar.issue.ignore.multicriteria.e16.resourceKey=**/Tests/**/*.php
|
||||
sonar.issue.ignore.multicriteria.e17.ruleKey=php:S1192
|
||||
sonar.issue.ignore.multicriteria.e17.resourceKey=tests/**/*.php
|
||||
|
||||
# Laravel policy method signatures are framework contracts even when a particular action is unconditional.
|
||||
sonar.issue.ignore.multicriteria.e18.ruleKey=php:S1172
|
||||
sonar.issue.ignore.multicriteria.e18.resourceKey=Modules/Helpdesk/Policies/TicketPolicy.php
|
||||
|
||||
sonar.issue.ignore.multicriteria.e19.ruleKey=css:S4666
|
||||
sonar.issue.ignore.multicriteria.e19.resourceKey=resources/views/errors/*.blade.php
|
||||
sonar.issue.ignore.multicriteria.e20.ruleKey=php:S117
|
||||
sonar.issue.ignore.multicriteria.e20.resourceKey=Modules/Helpdesk/Policies/TicketPolicy.php
|
||||
@@ -10,6 +10,7 @@ use PHPUnit\Framework\Attributes\BeforeClass;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
private const LOOPBACK = self::LOOPBACK;
|
||||
use CreatesApplication;
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
if (! static::runningInSail()) {
|
||||
static::ensureChromeDriverBinaryAlias();
|
||||
static::startChromeDriver(['--port=9515']);
|
||||
static::waitForTcpPort('127.0.0.1', 9515, 8.0, 'ChromeDriver');
|
||||
static::waitForTcpPort(self::LOOPBACK, 9515, 8.0, 'ChromeDriver');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
$parts = parse_url($url);
|
||||
|
||||
$scheme = $parts['scheme'] ?? 'http';
|
||||
$host = $parts['host'] ?? '127.0.0.1';
|
||||
$host = $parts['host'] ?? self::LOOPBACK;
|
||||
$port = (int) ($parts['port'] ?? ($scheme === 'https' ? 443 : 80));
|
||||
|
||||
$useExternalServer = filter_var(
|
||||
@@ -191,7 +192,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
private static function availablePort(int $from, int $to): int
|
||||
{
|
||||
for ($port = $from; $port <= $to; $port++) {
|
||||
if (! static::canConnect('127.0.0.1', $port)) {
|
||||
if (! static::canConnect(self::LOOPBACK, $port)) {
|
||||
return $port;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +276,4 @@ class PlatformAdministrationTest extends TestCase
|
||||
/**
|
||||
* Seed the test session with the matching CSRF token.
|
||||
*/
|
||||
private function withCsrfSession(): self
|
||||
{
|
||||
$this->withSession(['_token' => 'test-token']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,10 +314,4 @@ class PlatformPriceCrudTest extends TestCase
|
||||
/**
|
||||
* Seed the test session with the matching CSRF token.
|
||||
*/
|
||||
private function withCsrfSession(): self
|
||||
{
|
||||
$this->withSession(['_token' => 'test-token']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class ProfileUpdateTest extends TestCase
|
||||
|
||||
public function test_profile_page_is_displayed(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$this->get('/settings/profile')->assertOk();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user