added countries support expanded for ISO 3166 standard merged in new support
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Countries\Http\Controllers\Api\CountriesApiController;
|
||||
use Modules\Countries\Http\Controllers\Api\ReferenceDataApiController;
|
||||
|
||||
Route::prefix('api/v1/countries')->name('api.countries.')->group(function (): void {
|
||||
Route::get('/', [CountriesApiController::class, 'index'])->name('index');
|
||||
Route::get('/active', [CountriesApiController::class, 'active'])->name('active');
|
||||
Route::get('/resolve/{code}', [CountriesApiController::class, 'resolve'])
|
||||
->where('code', '[A-Za-z0-9]{2,3}')
|
||||
->name('resolve');
|
||||
Route::get('/{country:alpha2}', [CountriesApiController::class, 'show'])->name('show');
|
||||
Route::get('/{alpha2}/subdivisions', [CountriesApiController::class, 'subdivisions'])
|
||||
->where('alpha2', '[A-Za-z]{2}')
|
||||
->name('subdivisions');
|
||||
});
|
||||
|
||||
|
||||
Route::prefix('api/v1')->name('api.reference.')->group(function (): void {
|
||||
Route::get('/currencies', [ReferenceDataApiController::class, 'currencies'])->name('currencies.index');
|
||||
Route::get('/languages', [ReferenceDataApiController::class, 'languages'])->name('languages.index');
|
||||
Route::get('/scripts', [ReferenceDataApiController::class, 'scripts'])->name('scripts.index');
|
||||
Route::get('/locodes', [ReferenceDataApiController::class, 'locodes'])->name('locodes.index');
|
||||
});
|
||||
@@ -14,6 +14,17 @@ class DocumentationModuleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Add a CSRF token for projects that keep web middleware enabled during tests.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function csrfPayload(array $payload): array
|
||||
{
|
||||
return array_merge(['_token' => 'test-token'], $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* The documentation dashboard renders for authenticated users.
|
||||
*/
|
||||
@@ -40,7 +51,7 @@ class DocumentationModuleTest extends TestCase
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('documentation.documents.store'), [
|
||||
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
|
||||
'documentation_category_id' => $category->id,
|
||||
'title' => 'Goods In Process',
|
||||
'slug' => 'goods-in-process',
|
||||
@@ -50,7 +61,7 @@ class DocumentationModuleTest extends TestCase
|
||||
'version' => '1.0.0',
|
||||
'reference' => 'SOP-001',
|
||||
'is_featured' => '1',
|
||||
]);
|
||||
]));
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('documentation_documents', [
|
||||
@@ -72,7 +83,7 @@ class DocumentationModuleTest extends TestCase
|
||||
$user = \App\Models\User::factory()->create();
|
||||
$file = UploadedFile::fake()->createWithContent('goods-in.md', "# Goods In\n\n- Scan delivery note\n- Confirm quantities");
|
||||
|
||||
$response = $this->actingAs($user)->post(route('documentation.documents.store'), [
|
||||
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
|
||||
'title' => 'Goods In Markdown',
|
||||
'slug' => 'goods-in-markdown',
|
||||
'summary' => 'Markdown controlled document.',
|
||||
@@ -80,7 +91,7 @@ class DocumentationModuleTest extends TestCase
|
||||
'status' => DocumentationDocument::STATUS_PUBLISHED,
|
||||
'version' => '1.0.0',
|
||||
'attachment' => $file,
|
||||
]);
|
||||
]));
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
@@ -102,7 +113,7 @@ class DocumentationModuleTest extends TestCase
|
||||
|
||||
$user = \App\Models\User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('documentation.documents.store'), [
|
||||
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
|
||||
'title' => 'Attachment Pack',
|
||||
'slug' => 'attachment-pack',
|
||||
'summary' => 'Document with supporting files.',
|
||||
@@ -113,7 +124,7 @@ class DocumentationModuleTest extends TestCase
|
||||
UploadedFile::fake()->createWithContent('setup.md', "```php\necho 'ok';\n```"),
|
||||
UploadedFile::fake()->create('process.pdf', 64, 'application/pdf'),
|
||||
],
|
||||
]);
|
||||
]));
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
@@ -129,7 +140,8 @@ class DocumentationModuleTest extends TestCase
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete(route('documentation.documents.attachments.destroy', [$document, $attachment]))
|
||||
->withSession(['_token' => 'test-token'])
|
||||
->delete(route('documentation.documents.attachments.destroy', [$document, $attachment]), ['_token' => 'test-token'])
|
||||
->assertRedirect(route('documentation.documents.edit', $document));
|
||||
|
||||
$this->assertDatabaseMissing('documentation_attachments', ['id' => $attachment->id]);
|
||||
@@ -158,14 +170,14 @@ class DocumentationModuleTest extends TestCase
|
||||
{
|
||||
$user = \App\Models\User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->post(route('documentation.categories.store'), [
|
||||
$this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.categories.store'), $this->csrfPayload([
|
||||
'name' => 'Policies',
|
||||
'slug' => 'policies',
|
||||
'colour' => 'emerald',
|
||||
'description' => 'Controlled business policies.',
|
||||
'sort_order' => 1,
|
||||
'is_active' => '1',
|
||||
])->assertRedirect(route('documentation.categories.index'));
|
||||
]))->assertRedirect(route('documentation.categories.index'));
|
||||
|
||||
$this->assertDatabaseHas('documentation_categories', [
|
||||
'name' => 'Policies',
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Countries\Http\Controllers\Api\CountryLookupController;
|
||||
|
||||
if (class_exists(CountryLookupController::class)) {
|
||||
Route::get('/countries', [CountryLookupController::class, 'index'])->name('api.countries.index');
|
||||
Route::get('/countries/{country:alpha2}', [CountryLookupController::class, 'show'])->name('api.countries.show');
|
||||
}
|
||||
@@ -59,6 +59,7 @@ final class TicketIndex extends Component
|
||||
private function tickets(): LengthAwarePaginator
|
||||
{
|
||||
$user = Auth::user();
|
||||
$search = trim($this->search);
|
||||
|
||||
return Ticket::query()
|
||||
->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])
|
||||
@@ -67,11 +68,11 @@ final class TicketIndex extends Component
|
||||
->when($this->scope === 'closed', static fn (Builder $query): Builder => $query->whereNotNull('completed_at'))
|
||||
->when($this->status !== '', fn (Builder $query): Builder => $query->where('status_id', (int) $this->status))
|
||||
->when($this->priority !== '', fn (Builder $query): Builder => $query->where('priority_id', (int) $this->priority))
|
||||
->when($this->search !== '', function (Builder $query): Builder {
|
||||
return $query->where(static function (Builder $subQuery): void {
|
||||
$subQuery->where('reference', 'like', '%'.$this->search.'%')
|
||||
->orWhere('subject', 'like', '%'.$this->search.'%')
|
||||
->orWhere('body', 'like', '%'.$this->search.'%');
|
||||
->when($search !== '', static function (Builder $query) use ($search): Builder {
|
||||
return $query->where(static function (Builder $subQuery) use ($search): void {
|
||||
$subQuery->where('reference', 'like', '%'.$search.'%')
|
||||
->orWhere('subject', 'like', '%'.$search.'%')
|
||||
->orWhere('body', 'like', '%'.$search.'%');
|
||||
});
|
||||
})
|
||||
->latest('last_activity_at')
|
||||
|
||||
@@ -29,8 +29,7 @@ final class TicketPolicy
|
||||
public function update(User $user, Ticket $ticket): bool
|
||||
{
|
||||
return $this->isPrivileged($user)
|
||||
|| (int) $ticket->agent_id === (int) $user->id
|
||||
|| (int) $ticket->user_id === (int) $user->id;
|
||||
|| ((bool) ($user->helpdesk_agent ?? false) && (int) $ticket->agent_id === (int) $user->id);
|
||||
}
|
||||
|
||||
public function comment(User $user, Ticket $ticket): bool
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
@include('helpdesk::partials.page-header', [
|
||||
'eyebrow' => 'Helpdesk',
|
||||
'title' => 'Create Ticket',
|
||||
'title' => 'Create ticket',
|
||||
'subtitle' => 'Raise a new support request with priority, project, category, team and full details.',
|
||||
])
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ final class AgentAssignmentService
|
||||
public function selectAgentId(?int $categoryId): ?int
|
||||
{
|
||||
$query = User::query()
|
||||
->select('users.*')
|
||||
->where(static function (Builder $query): void {
|
||||
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
||||
})
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
]"
|
||||
/>
|
||||
|
||||
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
@foreach ([['Companies', $companiesCount], ['Active', $activeCompaniesCount], ['Warehouses', $warehousesCount], ['Prices', $pricesCount], ['Licences', $licensesCount]] as [$label, $value])
|
||||
<article class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ $label }}</p>
|
||||
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-white">{{ $value }}</p>
|
||||
</article>
|
||||
@endforeach
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 xl:grid-cols-3">
|
||||
<article class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div class="mb-5 flex items-start justify-between gap-4">
|
||||
@@ -79,6 +88,81 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div class="mb-5 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-bold uppercase tracking-[0.28em] text-cyan-500">Pricing</p>
|
||||
|
||||
<h2 class="mt-2 text-xl font-bold text-zinc-900 dark:text-white">
|
||||
Pricing plans
|
||||
</h2>
|
||||
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Current platform pricing tiers and administration shortcuts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
href="{{ route('platform.pricing.index') }}"
|
||||
wire:navigate
|
||||
class="inline-flex h-10 items-center justify-center gap-2 rounded-xl border border-zinc-200 px-4 text-sm font-bold text-zinc-700 hover:border-cyan-400 hover:text-cyan-700 dark:border-zinc-700 dark:text-zinc-200"
|
||||
>
|
||||
<i class="fa-solid fa-credit-card" aria-hidden="true"></i>
|
||||
View pricing
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{ route('platform.prices.index') }}"
|
||||
wire:navigate
|
||||
class="inline-flex h-10 items-center justify-center gap-2 rounded-xl bg-cyan-600 px-4 text-sm font-bold text-white shadow-sm shadow-cyan-600/20 ring-1 ring-cyan-500/30 transition hover:bg-cyan-500"
|
||||
>
|
||||
<i class="fa-solid fa-tags" aria-hidden="true"></i>
|
||||
Manage prices
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
@foreach ($prices as $price)
|
||||
<article class="rounded-2xl border border-zinc-200 p-5 dark:border-zinc-800">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-zinc-900 dark:text-white">
|
||||
{{ $price->name }}
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $price->strapline }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($price->badge_label)
|
||||
<span class="rounded-full bg-cyan-500/10 px-3 py-1 text-xs font-bold uppercase tracking-wide text-cyan-600 dark:text-cyan-400">
|
||||
{{ $price->badge_label }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-2xl font-black text-zinc-900 dark:text-white">
|
||||
£{{ number_format($price->monthly_amount / 100, 0) }}
|
||||
<span class="text-sm font-semibold text-zinc-500">/ month</span>
|
||||
</p>
|
||||
|
||||
@if (! empty($price->features))
|
||||
<ul class="mt-4 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
@foreach (array_slice($price->features, 0, 3) as $feature)
|
||||
<li class="flex gap-2">
|
||||
<i class="fa-solid fa-check mt-1 text-cyan-500" aria-hidden="true"></i>
|
||||
<span>{{ $feature }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<article class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
beStrictAboutTestsThatDoNotTestAnything="false"
|
||||
colors="true"
|
||||
processIsolation="false"
|
||||
stopOnError="false"
|
||||
stopOnFailure="false"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
backupStaticProperties="false">
|
||||
<testsuites>
|
||||
<testsuite name="Browser Test Suite">
|
||||
<directory suffix="Test.php">./tests/Browser</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
+58
-14
@@ -5,12 +5,14 @@ namespace Tests;
|
||||
use Facebook\WebDriver\Chrome\ChromeOptions;
|
||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Laravel\Dusk\TestCase as BaseTestCase;
|
||||
use PHPUnit\Framework\Attributes\BeforeClass;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
use CreatesApplication;
|
||||
|
||||
/**
|
||||
* Prepare for Dusk test execution.
|
||||
*/
|
||||
@@ -18,6 +20,7 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
public static function prepare(): void
|
||||
{
|
||||
if (! static::runningInSail()) {
|
||||
static::ensureChromeDriverBinaryAlias();
|
||||
static::startChromeDriver(['--port=9515']);
|
||||
}
|
||||
}
|
||||
@@ -27,22 +30,63 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
*/
|
||||
protected function driver(): RemoteWebDriver
|
||||
{
|
||||
$options = (new ChromeOptions)->addArguments(collect([
|
||||
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
|
||||
$options = (new ChromeOptions())->addArguments([
|
||||
'--disable-gpu',
|
||||
'--headless=new',
|
||||
'--window-size=1920,1080',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-search-engine-choice-screen',
|
||||
'--disable-smooth-scrolling',
|
||||
])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
|
||||
return $items->merge([
|
||||
'--disable-gpu',
|
||||
'--headless=new',
|
||||
]);
|
||||
})->all());
|
||||
]);
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
$_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
|
||||
DesiredCapabilities::chrome()->setCapability(
|
||||
ChromeOptions::CAPABILITY, $options
|
||||
)
|
||||
$_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
|
||||
DesiredCapabilities::chrome()->setCapability(ChromeOptions::CAPABILITY, $options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dusk may install a differently named macOS Apple Silicon binary.
|
||||
* Laravel Dusk still asks for chromedriver-mac-arm, so create that alias safely.
|
||||
*/
|
||||
private static function ensureChromeDriverBinaryAlias(): void
|
||||
{
|
||||
$binDirectory = base_path('vendor/laravel/dusk/bin');
|
||||
$expected = $binDirectory.'/chromedriver-mac-arm';
|
||||
|
||||
if (is_file($expected)) {
|
||||
@chmod($expected, 0755);
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_dir($binDirectory)) {
|
||||
@mkdir($binDirectory, 0755, true);
|
||||
}
|
||||
|
||||
try {
|
||||
Artisan::call('dusk:chrome-driver');
|
||||
} catch (\Throwable) {
|
||||
// Continue and inspect whatever files are available locally.
|
||||
}
|
||||
|
||||
$candidates = array_values(array_filter(glob($binDirectory.'/chromedriver*') ?: [], static function (string $path) use ($expected): bool {
|
||||
return is_file($path)
|
||||
&& $path !== $expected
|
||||
&& ! str_ends_with($path, '.zip')
|
||||
&& ! str_ends_with($path, '.txt');
|
||||
}));
|
||||
|
||||
usort($candidates, static function (string $left, string $right): int {
|
||||
$score = static fn (string $path): int => str_contains($path, 'mac-arm64') ? 0
|
||||
: (str_contains($path, 'mac-arm') ? 1
|
||||
: (str_contains($path, 'mac') ? 2 : 3));
|
||||
|
||||
return $score($left) <=> $score($right);
|
||||
});
|
||||
|
||||
if ($candidates !== []) {
|
||||
@copy($candidates[0], $expected);
|
||||
@chmod($expected, 0755);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user