countries selection fixed

This commit is contained in:
Mark Gregory
2026-07-07 15:55:17 +01:00
parent d90c945e83
commit ec27834043
20 changed files with 945 additions and 299 deletions
@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('company_addresses', function (Blueprint $table): void {
$table->id();
$table->morphs('addressable');
$table->string('label', 80)->default('Registered office');
$table->string('attention')->nullable();
$table->string('line_1');
$table->string('line_2')->nullable();
$table->string('line_3')->nullable();
$table->string('locality')->comment('City, town or locality.');
$table->string('administrative_area')->nullable()->comment('County, state, province or region.');
$table->string('postal_code', 32)->nullable();
$table->foreignId('country_id')->constrained('countries')->restrictOnDelete();
$table->foreignId('country_subdivision_id')->nullable()->constrained('country_subdivisions')->nullOnDelete();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->boolean('is_primary')->default(true);
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['addressable_type', 'addressable_id'], 'company_addresses_one_per_owner_unique');
$table->index(['country_id', 'locality']);
$table->index(['postal_code']);
});
}
public function down(): void
{
Schema::dropIfExists('company_addresses');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('company_divisions', function (Blueprint $table): void {
$table->id();
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('code', 40)->nullable();
$table->string('slug');
$table->string('status', 32)->default('active')->index();
$table->text('description')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['company_id', 'slug']);
$table->unique(['company_id', 'code']);
$table->index(['company_id', 'status', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('company_divisions');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('company_division_user', function (Blueprint $table): void {
$table->id();
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
$table->foreignId('company_division_id')->constrained('company_divisions')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('role', 80)->default('member')->index();
$table->boolean('is_director')->default(false)->index();
$table->timestamps();
$table->unique(['company_division_id', 'user_id']);
$table->index(['company_id', 'user_id']);
$table->index(['company_id', 'is_director']);
});
}
public function down(): void
{
Schema::dropIfExists('company_division_user');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('company_director_assignments', function (Blueprint $table): void {
$table->id();
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
$table->foreignId('company_division_id')->nullable()->constrained('company_divisions')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('coverage_type', 32)->default('company')->index();
$table->date('starts_at')->nullable();
$table->date('ends_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->unique(['company_id', 'company_division_id', 'user_id'], 'company_director_coverage_unique');
$table->index(['company_id', 'coverage_type']);
$table->index(['user_id', 'coverage_type']);
});
}
public function down(): void
{
Schema::dropIfExists('company_director_assignments');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('warehouses', function (Blueprint $table): void {
if (! Schema::hasColumn('warehouses', 'division_id')) {
$table->foreignId('division_id')->nullable()->after('company_id')->constrained('company_divisions')->nullOnDelete();
$table->index(['company_id', 'division_id']);
}
});
}
public function down(): void
{
Schema::table('warehouses', function (Blueprint $table): void {
if (Schema::hasColumn('warehouses', 'division_id')) {
$table->dropConstrainedForeignId('division_id');
}
});
}
};
@@ -0,0 +1,61 @@
<?php
namespace Modules\Company\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Modules\Company\Models\Company;
use Modules\Company\Models\CompanyDivision;
use Modules\Warehouse\app\Models\Warehouse;
class CompanyAddressController extends Controller
{
public function updateCompany(Request $request, Company $company)
{
$this->upsertAddress($request, $company);
return redirect()->route('platform.companies.show', $company)->with('status', 'Company address updated.');
}
public function updateDivision(Request $request, Company $company, CompanyDivision $division)
{
abort_unless($division->company_id === $company->id, 404);
$this->upsertAddress($request, $division, 'Operating address');
return redirect()->route('platform.companies.show', $company)->with('status', 'Division address updated.');
}
public function updateWarehouse(Request $request, Company $company, Warehouse $warehouse)
{
abort_unless($warehouse->company_id === $company->id, 404);
$this->upsertAddress($request, $warehouse, 'Warehouse address');
return redirect()->route('platform.companies.show', $company)->with('status', 'Warehouse address updated.');
}
private function upsertAddress(Request $request, object $owner, string $defaultLabel = 'Registered office'): void
{
$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'],
'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'],
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
]);
$data['label'] = $data['label'] ?: $defaultLabel;
$data['is_primary'] = true;
$owner->address()->updateOrCreate([], $data);
}
}
@@ -4,7 +4,6 @@ namespace Modules\Company\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Modules\Company\Models\Company;
@@ -17,24 +16,12 @@ class CompanyController extends Controller
*/
public function index()
{
$counts = ['users', 'warehouses', 'licenseKeys'];
if (Schema::hasTable('company_divisions')) {
$counts[] = 'divisions';
}
$companies = Company::query()
->with(['country'])
->withCount($counts)
->orderBy('name')
->paginate(20);
$companies->getCollection()->each(function (Company $company): void {
$company->setAttribute('divisions_count', $company->getAttribute('divisions_count') ?? 0);
});
return view('company::index', [
'companies' => $companies,
'companies' => Company::query()
->with(['address.country', 'country'])
->withCount(['users', 'warehouses', 'licenseKeys', 'divisions'])
->orderBy('name')
->paginate(20),
'countries' => $this->countries(),
]);
}
@@ -70,20 +57,17 @@ class CompanyController extends Controller
*/
public function show(Company $company)
{
$loads = ['country'];
$counts = ['users', 'warehouses', 'licenseKeys', 'payments'];
if (Schema::hasTable('company_divisions')) {
$loads[] = 'divisions';
$counts[] = 'divisions';
}
if (Schema::hasTable('company_director_assignments')) {
$loads[] = 'directorAssignments.user';
}
$company->load($loads)->loadCount($counts);
$company->setAttribute('divisions_count', $company->getAttribute('divisions_count') ?? 0);
$company->load([
'address.country',
'country',
'divisions.address.country',
'divisions.warehouses',
'divisions.users',
'warehouses.address.country',
'warehouses.division',
'directorAssignments.user',
'directorAssignments.division',
])->loadCount(['users', 'warehouses', 'licenseKeys', 'payments', 'divisions']);
return view('company::show', [
'company' => $company,
@@ -91,6 +75,8 @@ class CompanyController extends Controller
'recentWarehouses' => $company->warehouses()->latest('id')->limit(5)->get(),
'recentLicenseKeys' => $company->licenseKeys()->latest('id')->limit(5)->get(),
'countries' => $this->countries(),
'companyUsers' => $company->users()->orderBy('name')->get(['id', 'name', 'email', 'company_id']),
'unassignedWarehouses' => $company->warehouses()->whereNull('division_id')->orderBy('name')->get(),
]);
}
@@ -100,7 +86,7 @@ class CompanyController extends Controller
public function edit(Company $company)
{
return view('company::edit', [
'company' => $company->loadMissing('country'),
'company' => $company->loadMissing(['address.country', 'country']),
'countries' => $this->countries(),
]);
}
@@ -140,14 +126,10 @@ class CompanyController extends Controller
'country_id' => ['nullable', Rule::exists('countries', 'id')->where('is_active', true)],
]);
return [
'name' => $data['name'],
'billing_email' => $data['billing_email'] ?? null,
'status' => $data['status'],
'country_id' => $data['country_id'] ?? null,
'slug' => Str::slug($data['name']),
'timezone' => $company?->timezone ?: 'Europe/London',
];
$data['slug'] = Str::slug($data['name']);
$data['timezone'] = $company?->timezone ?: 'Europe/London';
return $data;
}
private function countries()
@@ -155,6 +137,6 @@ class CompanyController extends Controller
return Country::query()
->active()
->orderByRaw("LOWER(COALESCE(NULLIF(display_name, ''), NULLIF(common_name, ''), name)) ASC")
->get(['id', 'alpha2', 'name', 'display_name', 'common_name']);
->get(['id', 'alpha2', 'name', 'display_name', 'common_name', 'sort_order']);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Modules\Company\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Modules\Company\Models\Company;
use Modules\Company\Models\CompanyDivision;
class CompanyDivisionController extends Controller
{
public function store(Request $request, Company $company)
{
$division = $company->divisions()->create($this->validated($request, $company));
return redirect()->route('platform.companies.show', $company)->with('status', "Division {$division->name} created.");
}
public function update(Request $request, Company $company, CompanyDivision $division)
{
abort_unless($division->company_id === $company->id, 404);
$division->update($this->validated($request, $company, $division));
return redirect()->route('platform.companies.show', $company)->with('status', 'Division updated.');
}
public function destroy(Company $company, CompanyDivision $division)
{
abort_unless($division->company_id === $company->id, 404);
abort_if($division->warehouses()->exists(), 422, 'Move warehouses before deleting this division.');
$division->delete();
return redirect()->route('platform.companies.show', $company)->with('status', 'Division deleted.');
}
private function validated(Request $request, Company $company, ?CompanyDivision $division = null): array
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'code' => ['nullable', 'string', 'max:40', Rule::unique('company_divisions', 'code')->where('company_id', $company->id)->ignore($division?->id)],
'status' => ['required', Rule::in(['active', 'suspended', 'inactive'])],
'description' => ['nullable', 'string', 'max:2000'],
]);
$data['slug'] = Str::slug($data['name']);
return $data;
}
}
@@ -0,0 +1,83 @@
<?php
namespace Modules\Company\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Modules\Company\Models\Company;
use Modules\Company\Models\CompanyDivision;
use Modules\Company\Models\CompanyDirectorAssignment;
class CompanyMembershipController extends Controller
{
public function assignDivision(Request $request, Company $company)
{
$data = $request->validate([
'user_id' => ['required', Rule::exists('users', 'id')->where('company_id', $company->id)],
'company_division_id' => ['required', Rule::exists('company_divisions', 'id')->where('company_id', $company->id)],
'role' => ['nullable', 'string', 'max:80'],
'is_director' => ['nullable', 'boolean'],
]);
$division = CompanyDivision::where('company_id', $company->id)->findOrFail($data['company_division_id']);
$division->users()->syncWithoutDetaching([
$data['user_id'] => [
'company_id' => $company->id,
'role' => $data['role'] ?? 'member',
'is_director' => (bool) ($data['is_director'] ?? false),
],
]);
if ((bool) ($data['is_director'] ?? false)) {
CompanyDirectorAssignment::updateOrCreate([
'company_id' => $company->id,
'company_division_id' => $division->id,
'user_id' => $data['user_id'],
], ['coverage_type' => 'division']);
}
return redirect()->route('platform.companies.show', $company)->with('status', 'Division membership updated.');
}
public function assignDirector(Request $request, Company $company)
{
$data = $request->validate([
'user_id' => ['required', Rule::exists('users', 'id')->where('company_id', $company->id)],
'coverage_type' => ['required', Rule::in(['company', 'division'])],
'company_division_id' => ['nullable', Rule::exists('company_divisions', 'id')->where('company_id', $company->id)],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after_or_equal:starts_at'],
'notes' => ['nullable', 'string', 'max:2000'],
]);
if ($data['coverage_type'] === 'company') {
$data['company_division_id'] = null;
} else {
abort_if(empty($data['company_division_id']), 422, 'Division coverage requires a division.');
}
CompanyDirectorAssignment::updateOrCreate([
'company_id' => $company->id,
'company_division_id' => $data['company_division_id'],
'user_id' => $data['user_id'],
], [
'coverage_type' => $data['coverage_type'],
'starts_at' => $data['starts_at'] ?? null,
'ends_at' => $data['ends_at'] ?? null,
'notes' => $data['notes'] ?? null,
]);
return redirect()->route('platform.companies.show', $company)->with('status', 'Director coverage updated.');
}
public function removeDirector(Company $company, CompanyDirectorAssignment $assignment)
{
abort_unless($assignment->company_id === $company->id, 404);
$assignment->delete();
return redirect()->route('platform.companies.show', $company)->with('status', 'Director coverage removed.');
}
}
@@ -0,0 +1,25 @@
<?php
namespace Modules\Company\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Modules\Company\Models\Company;
use Modules\Warehouse\app\Models\Warehouse;
class CompanyWarehouseAssignmentController extends Controller
{
public function update(Request $request, Company $company, Warehouse $warehouse)
{
abort_unless($warehouse->company_id === $company->id, 404);
$data = $request->validate([
'division_id' => ['nullable', Rule::exists('company_divisions', 'id')->where('company_id', $company->id)],
]);
$warehouse->update(['division_id' => $data['division_id'] ?? null]);
return redirect()->route('platform.companies.show', $company)->with('status', 'Warehouse division assignment updated.');
}
}
+1 -1
View File
@@ -10,8 +10,8 @@ use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Modules\Platform\app\Models\LicenseKey;
use Modules\Platform\app\Models\Payment;
use Modules\Countries\Models\Country;
use Modules\Platform\app\Models\Payment;
use Modules\Warehouse\app\Models\Warehouse;
class Company extends Model
+28 -1
View File
@@ -6,13 +6,21 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Modules\Countries\Models\Country;
use Modules\Countries\Models\CountrySubdivision;
class CompanyAddress extends Model
{
protected $guarded = [];
protected $fillable = [
'addressable_type', 'addressable_id', 'label', 'attention', 'line_1', 'line_2', 'line_3',
'locality', 'administrative_area', 'postal_code', 'country_id', 'country_subdivision_id',
'latitude', 'longitude', 'is_primary', 'metadata',
];
protected $casts = [
'is_primary' => 'boolean',
'metadata' => 'array',
'latitude' => 'decimal:7',
'longitude' => 'decimal:7',
];
public function addressable(): MorphTo
@@ -24,4 +32,23 @@ class CompanyAddress extends Model
{
return $this->belongsTo(Country::class);
}
public function subdivision(): BelongsTo
{
return $this->belongsTo(CountrySubdivision::class, 'country_subdivision_id');
}
public function getFormattedAttribute(): string
{
return collect([
$this->attention,
$this->line_1,
$this->line_2,
$this->line_3,
$this->locality,
$this->administrative_area,
$this->postal_code,
$this->country?->effective_name ?? $this->country?->name,
])->filter()->implode(', ');
}
}
@@ -8,20 +8,27 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CompanyDirectorAssignment extends Model
{
protected $guarded = [];
protected $fillable = [
'company_id', 'company_division_id', 'user_id', 'coverage_type', 'starts_at', 'ends_at', 'notes',
];
protected $casts = [
'starts_at' => 'date',
'ends_at' => 'date',
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function division(): BelongsTo
{
return $this->belongsTo(CompanyDivision::class, 'company_division_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+30 -3
View File
@@ -5,13 +5,33 @@ namespace Modules\Company\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Modules\Warehouse\app\Models\Warehouse;
class CompanyDivision extends Model
{
protected $guarded = [];
use SoftDeletes;
protected $fillable = [
'company_id', 'uuid', 'name', 'code', 'slug', 'status', 'description', 'metadata',
];
protected $casts = [
'metadata' => 'array',
];
protected static function booted(): void
{
static::creating(function (CompanyDivision $division): void {
$division->uuid ??= (string) Str::uuid();
$division->slug ??= Str::slug($division->name);
$division->status ??= 'active';
});
}
public function company(): BelongsTo
{
@@ -23,13 +43,20 @@ class CompanyDivision extends Model
return $this->morphOne(CompanyAddress::class, 'addressable');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'company_division_user')
->withPivot(['company_id', 'role', 'is_director'])
->withTimestamps();
}
public function warehouses(): HasMany
{
return $this->hasMany(Warehouse::class, 'division_id');
}
public function users(): HasMany
public function directorAssignments(): HasMany
{
return $this->hasMany(User::class, 'division_id');
return $this->hasMany(CompanyDirectorAssignment::class, 'company_division_id');
}
}
@@ -0,0 +1,71 @@
@php
$address = $address ?? null;
$inputClass = 'mt-2 h-12 w-full rounded-xl border-zinc-300 bg-white px-4 text-sm text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white';
@endphp
<div class="grid gap-5 md:grid-cols-2">
<div>
<label for="address_label" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Label</label>
<input id="address_label" name="label" value="{{ old('label', $address?->label ?? $defaultLabel ?? 'Registered office') }}" class="{{ $inputClass }}">
</div>
<div>
<label for="address_attention" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Attention</label>
<input id="address_attention" name="attention" value="{{ old('attention', $address?->attention) }}" class="{{ $inputClass }}">
</div>
<div class="md:col-span-2">
<label for="address_line_1" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Address line 1</label>
<input id="address_line_1" name="line_1" value="{{ old('line_1', $address?->line_1) }}" class="{{ $inputClass }}" required>
</div>
<div>
<label for="address_line_2" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Address line 2</label>
<input id="address_line_2" name="line_2" value="{{ old('line_2', $address?->line_2) }}" class="{{ $inputClass }}">
</div>
<div>
<label for="address_line_3" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Address line 3</label>
<input id="address_line_3" name="line_3" value="{{ old('line_3', $address?->line_3) }}" class="{{ $inputClass }}">
</div>
<div>
<label for="address_locality" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Town / City</label>
<input id="address_locality" name="locality" value="{{ old('locality', $address?->locality) }}" class="{{ $inputClass }}" required>
</div>
<div>
<label for="address_administrative_area" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">County / State / Region</label>
<input id="address_administrative_area" name="administrative_area" value="{{ old('administrative_area', $address?->administrative_area) }}" class="{{ $inputClass }}">
</div>
<div>
<label for="address_postal_code" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Postcode / ZIP</label>
<input id="address_postal_code" name="postal_code" value="{{ old('postal_code', $address?->postal_code) }}" class="{{ $inputClass }}">
</div>
<div class="wms-select2-field min-w-0">
<label for="address_country_id" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Country</label>
<select
id="address_country_id"
name="country_id"
data-wms-select2
data-placeholder="Search and select country"
class="wms-select2-control"
required
>
<option value="">Search and select country</option>
@foreach ($countries as $country)
<option value="{{ $country->id }}" @selected((int) old('country_id', $address?->country_id) === $country->id)>
{{ $country->display_name ?: $country->common_name ?: $country->name }}
</option>
@endforeach
</select>
</div>
</div>
<div class="mt-6">
<button class="h-12 rounded-xl bg-green-600 px-5 text-sm font-semibold text-white shadow-sm shadow-green-950/20 transition hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/40">
Save address
</button>
</div>
+274 -51
View File
@@ -1,66 +1,289 @@
<x-layouts::app :title="$company->name">
<div class="space-y-6">
<x-wms.page-hero
eyebrow="Platform Administration"
:title="$company->name"
description="Company tenant overview."
icon="fa-solid fa-building"
/>
<x-wms.page-hero eyebrow="Company Workbench" :title="$company->name" description="Manage the tenant profile, address, divisions, director coverage and warehouse alignment." icon="fa-solid fa-building" />
@if (session('status'))
<div class="rounded-2xl bg-green-50 p-4 text-sm font-semibold text-green-700 ring-1 ring-green-200 dark:bg-green-950 dark:text-green-300 dark:ring-green-900">
{{ session('status') }}
</div>
@endif
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<div class="rounded-3xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><p class="text-sm text-zinc-500">Users</p><p class="mt-2 text-3xl font-bold">{{ $company->users_count }}</p></div>
<div class="rounded-3xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><p class="text-sm text-zinc-500">Divisions</p><p class="mt-2 text-3xl font-bold">{{ $company->divisions_count }}</p></div>
<div class="rounded-3xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><p class="text-sm text-zinc-500">Warehouses</p><p class="mt-2 text-3xl font-bold">{{ $company->warehouses_count }}</p></div>
<div class="rounded-3xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><p class="text-sm text-zinc-500">Licences</p><p class="mt-2 text-3xl font-bold">{{ $company->license_keys_count }}</p></div>
<div class="rounded-3xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><p class="text-sm text-zinc-500">Payments</p><p class="mt-2 text-3xl font-bold">{{ $company->payments_count }}</p></div>
</div>
<div class="grid gap-4 md:grid-cols-4">
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<p class="text-xs font-bold uppercase tracking-wide text-zinc-500">Users</p>
<p class="mt-2 text-2xl font-bold text-zinc-900 dark:text-white">{{ $company->users_count ?? 0 }}</p>
<div class="grid gap-6 xl:grid-cols-3">
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800 xl:col-span-2">
<div class="flex items-start justify-between gap-4">
<div><h2 class="text-lg font-bold text-zinc-900 dark:text-white">Company profile</h2><p class="mt-1 text-sm text-zinc-500">Core tenant identity and operational status.</p></div>
<a href="{{ route('platform.companies.edit', $company) }}" class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white">Edit company</a>
</div>
<dl class="mt-6 grid gap-5 md:grid-cols-2">
<div><dt class="text-sm text-zinc-500">Billing email</dt><dd class="font-semibold">{{ $company->billing_email ?? '—' }}</dd></div>
<div><dt class="text-sm text-zinc-500">Status</dt><dd class="font-semibold">{{ $company->is_system_company ? 'System' : ucfirst($company->status) }}</dd></div>
<div><dt class="text-sm text-zinc-500">Timezone</dt><dd class="font-semibold">{{ $company->timezone }}</dd></div>
<div><dt class="text-sm text-zinc-500">Country</dt><dd class="font-semibold">{{ $company->country?->display_name ?: $company->country?->common_name ?: $company->country?->name ?: '—' }}</dd></div>
<div><dt class="text-sm text-zinc-500">Slug</dt><dd class="font-semibold">{{ $company->slug }}</dd></div>
</dl>
</div>
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<p class="text-xs font-bold uppercase tracking-wide text-zinc-500">Divisions</p>
<p class="mt-2 text-2xl font-bold text-zinc-900 dark:text-white">{{ $company->divisions_count ?? 0 }}</p>
</div>
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<p class="text-xs font-bold uppercase tracking-wide text-zinc-500">Warehouses</p>
<p class="mt-2 text-2xl font-bold text-zinc-900 dark:text-white">{{ $company->warehouses_count ?? 0 }}</p>
</div>
<div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<p class="text-xs font-bold uppercase tracking-wide text-zinc-500">Licences</p>
<p class="mt-2 text-2xl font-bold text-zinc-900 dark:text-white">{{ $company->license_keys_count ?? 0 }}</p>
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h2 class="text-lg font-bold text-zinc-900 dark:text-white">Quick actions</h2>
<div class="mt-5 space-y-3">
<a href="{{ route('platform.companies.index') }}" class="block rounded-xl border border-zinc-300 px-4 py-2 text-center text-sm font-semibold">Back to companies</a>
@unless ($company->is_system_company)
<form method="POST" action="{{ route('platform.companies.destroy', $company) }}" onsubmit="return confirm('Delete this company? This cannot be undone.');">@csrf @method('DELETE')<button class="w-full rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white">Delete company</button></form>
@endunless
</div>
</div>
</div>
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 class="text-lg font-bold text-zinc-900 dark:text-white">Company details</h2>
<dl class="mt-4 grid gap-4 text-sm md:grid-cols-2">
<div>
<dt class="font-semibold text-zinc-500">Billing email</dt>
<dd class="mt-1 text-zinc-900 dark:text-white">{{ $company->billing_email ?: 'Not set' }}</dd>
</div>
<div>
<dt class="font-semibold text-zinc-500">Country</dt>
<dd class="mt-1 text-zinc-900 dark:text-white">{{ $company->country?->display_name ?: $company->country?->common_name ?: $company->country?->name ?: 'Not set' }}</dd>
</div>
<div>
<dt class="font-semibold text-zinc-500">Status</dt>
<dd class="mt-1 text-zinc-900 dark:text-white">{{ ucfirst($company->status) }}</dd>
</div>
<div>
<dt class="font-semibold text-zinc-500">Timezone</dt>
<dd class="mt-1 text-zinc-900 dark:text-white">{{ $company->timezone }}</dd>
</div>
</dl>
</div>
<h2 class="text-lg font-bold text-zinc-900 dark:text-white">Company address</h2>
@if ($company->address)
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-300">{{ $company->address->formatted }}</p>
@else
<p class="mt-2 text-sm text-zinc-500">No company address has been recorded.</p>
@endif
<form method="POST" action="{{ route('platform.companies.address.update', $company) }}" class="mt-5">
@csrf @method('PUT')
@include('company::partials.address-form', ['address' => $company->address, 'defaultLabel' => 'Registered office'])
</form>
</div>
<div class="flex gap-2">
<a href="{{ route('platform.companies.edit', $company) }}" class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">Edit</a>
<a href="{{ route('platform.companies.index') }}" class="rounded-xl border border-zinc-300 px-4 py-2 text-sm font-semibold text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800">Back</a>
<div class="grid gap-6 xl:grid-cols-2">
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h2 class="text-lg font-bold text-zinc-900 dark:text-white">Create division</h2>
<form method="POST" action="{{ route('platform.companies.divisions.store', $company) }}" class="mt-5 space-y-4">
@csrf
<div class="grid gap-4 md:grid-cols-2">
<div>
<label for="division_name" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Division name</label>
<input
id="division_name"
name="name"
value="{{ old('name') }}"
placeholder="Division name"
class="mt-2 h-12 w-full rounded-xl border-zinc-300 bg-white px-4 text-sm font-medium text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
required
>
</div>
<div>
<label for="division_code" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Code</label>
<input
id="division_code"
name="code"
value="{{ old('code') }}"
placeholder="Optional code"
class="mt-2 h-12 w-full rounded-xl border-zinc-300 bg-white px-4 text-sm font-medium text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
>
</div>
<div class="wms-select2-field">
<label for="division_status" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Status</label>
<select
id="division_status"
name="status"
data-wms-select2
data-placeholder="Select status"
class="wms-select2-control"
>
<option value="active" @selected(old('status', 'active') === 'active')>Active</option>
<option value="suspended" @selected(old('status') === 'suspended')>Suspended</option>
<option value="inactive" @selected(old('status') === 'inactive')>Inactive</option>
</select>
</div>
<div class="flex items-end">
<button class="h-12 w-full rounded-xl bg-green-600 px-4 text-sm font-bold text-white shadow-sm shadow-green-950/20 transition hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/40">
Create division
</button>
</div>
</div>
<div>
<label for="division_description" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Description</label>
<textarea
id="division_description"
name="description"
placeholder="Description"
rows="3"
class="mt-2 w-full rounded-xl border-zinc-300 bg-white px-4 py-3 text-sm font-medium text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
>{{ old('description') }}</textarea>
</div>
</form>
</div>
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h2 class="text-lg font-bold text-zinc-900 dark:text-white">Director coverage</h2>
<form method="POST" action="{{ route('platform.companies.director-assignments.store', $company) }}" class="mt-5 space-y-4">
@csrf
<div class="grid gap-4 md:grid-cols-2">
<div class="wms-select2-field">
<label for="director_user_id" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">User</label>
<select
id="director_user_id"
name="user_id"
data-wms-select2
data-placeholder="Select user"
class="wms-select2-control"
required
>
<option value="">Select user</option>
@foreach($companyUsers as $user)
<option value="{{ $user->id }}" @selected(old('user_id') == $user->id)>
{{ $user->name }}{{ $user->email ? ' — '.$user->email : '' }}
</option>
@endforeach
</select>
</div>
<div class="wms-select2-field">
<label for="director_coverage_type" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Coverage</label>
<select
id="director_coverage_type"
name="coverage_type"
data-wms-select2
data-placeholder="Select coverage"
class="wms-select2-control"
>
<option value="company" @selected(old('coverage_type', 'company') === 'company')>Whole company</option>
<option value="division" @selected(old('coverage_type') === 'division')>Specific division</option>
</select>
</div>
<div class="wms-select2-field">
<label for="director_company_division_id" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Division</label>
<select
id="director_company_division_id"
name="company_division_id"
data-wms-select2
data-placeholder="No division"
class="wms-select2-control"
>
<option value="">No division</option>
@foreach($company->divisions->sortBy('name') as $division)
<option value="{{ $division->id }}" @selected(old('company_division_id') == $division->id)>{{ $division->name }}</option>
@endforeach
</select>
</div>
<div class="flex items-end">
<button class="h-12 w-full rounded-xl bg-green-600 px-4 text-sm font-bold text-white shadow-sm shadow-green-950/20 transition hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/40">
Assign director
</button>
</div>
</div>
</form>
<div class="mt-5 space-y-3">
@forelse($company->directorAssignments as $assignment)
<div class="flex items-center justify-between rounded-2xl border border-zinc-200 p-3 dark:border-zinc-800">
<span>{{ $assignment->user?->name }} — {{ $assignment->coverage_type === 'company' ? 'Whole company' : $assignment->division?->name }}</span>
<form method="POST" action="{{ route('platform.companies.director-assignments.destroy', [$company, $assignment]) }}">
@csrf
@method('DELETE')
<button class="text-sm font-semibold text-red-600">Remove</button>
</form>
</div>
@empty
<p class="text-sm text-zinc-500">No director coverage recorded.</p>
@endforelse
</div>
</div>
</div>
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h2 class="text-lg font-bold text-zinc-900 dark:text-white">Warehouse division and address alignment</h2>
<p class="mt-1 text-sm text-zinc-500">Warehouses remain tenant-scoped to the company. Assign a division only when that operating structure exists and record an optional warehouse operating address where needed.</p>
<div class="mt-5 space-y-5">
@forelse($company->warehouses as $warehouse)
<div class="rounded-2xl border border-zinc-200 p-4 dark:border-zinc-800">
<div class="grid gap-3 md:grid-cols-3">
<div>
<p class="font-semibold text-zinc-900 dark:text-white">{{ $warehouse->name }}</p>
<p class="text-sm text-zinc-500">{{ $warehouse->code }} · {{ $warehouse->division?->name ?? 'Company-level warehouse' }}</p>
@if ($warehouse->address)
<p class="mt-2 text-xs text-zinc-500">{{ $warehouse->address->formatted }}</p>
@else
<p class="mt-2 text-xs text-zinc-500">No warehouse address recorded.</p>
@endif
</div>
<form method="POST" action="{{ route('platform.companies.warehouses.division.update', [$company, $warehouse]) }}" class="contents">
@csrf @method('PUT')
<select name="division_id" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
<option value="">Company-level warehouse</option>
@foreach($company->divisions as $divisionOption)
<option value="{{ $divisionOption->id }}" @selected($warehouse->division_id === $divisionOption->id)>{{ $divisionOption->name }}</option>
@endforeach
</select>
<button class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white">Update assignment</button>
</form>
</div>
<details class="mt-4 rounded-2xl bg-zinc-50 p-4 dark:bg-zinc-950">
<summary class="cursor-pointer text-sm font-semibold text-zinc-900 dark:text-white">Edit warehouse address</summary>
<form method="POST" action="{{ route('platform.companies.warehouses.address.update', [$company, $warehouse]) }}" class="mt-4">
@csrf @method('PUT')
@include('company::partials.address-form', ['address' => $warehouse->address, 'defaultLabel' => 'Warehouse address'])
</form>
</details>
</div>
@empty
<p class="text-sm text-zinc-500">No warehouses exist for this company yet.</p>
@endforelse
</div>
</div>
<div class="space-y-6">
<h2 class="text-xl font-bold text-zinc-900 dark:text-white">Divisions</h2>
@forelse($company->divisions as $division)
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<div class="flex flex-wrap items-start justify-between gap-4">
<div><h3 class="text-lg font-bold">{{ $division->name }}</h3><p class="text-sm text-zinc-500">{{ $division->code ?? 'No code' }} · {{ ucfirst($division->status) }} · {{ $division->warehouses->count() }} warehouses</p></div>
<form method="POST" action="{{ route('platform.companies.divisions.destroy', [$company, $division]) }}" onsubmit="return confirm('Delete this division?');">@csrf @method('DELETE')<button class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white">Delete division</button></form>
</div>
<form method="POST" action="{{ route('platform.companies.divisions.update', [$company, $division]) }}" class="mt-5 space-y-4">
@csrf
@method('PUT')
<div class="grid gap-4 md:grid-cols-4 md:items-end">
<div>
<label class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Division name</label>
<input name="name" value="{{ $division->name }}" class="mt-2 h-12 w-full rounded-xl border-zinc-300 bg-white px-4 text-sm font-medium text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white" required>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Code</label>
<input name="code" value="{{ $division->code }}" class="mt-2 h-12 w-full rounded-xl border-zinc-300 bg-white px-4 text-sm font-medium text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
</div>
<div class="wms-select2-field">
<label class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Status</label>
<select name="status" data-wms-select2 data-placeholder="Select status" class="wms-select2-control">
<option value="active" @selected($division->status === 'active')>Active</option>
<option value="suspended" @selected($division->status === 'suspended')>Suspended</option>
<option value="inactive" @selected($division->status === 'inactive')>Inactive</option>
</select>
</div>
<button class="h-12 rounded-xl bg-blue-600 px-4 text-sm font-bold text-white shadow-sm transition hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">Update division</button>
</div>
<div>
<label class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Description</label>
<textarea name="description" rows="3" class="mt-2 w-full rounded-xl border-zinc-300 bg-white px-4 py-3 text-sm font-medium text-zinc-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">{{ $division->description }}</textarea>
</div>
</form>
<div class="mt-6"><h4 class="font-semibold">Division address</h4><form method="POST" action="{{ route('platform.companies.divisions.address.update', [$company, $division]) }}" class="mt-4">@csrf @method('PUT')@include('company::partials.address-form', ['address' => $division->address, 'defaultLabel' => 'Operating address'])</form></div>
</div>
@empty
<div class="rounded-3xl bg-white p-6 text-sm text-zinc-500 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">No divisions exist. Warehouses can remain directly under the company until divisions are introduced.</div>
@endforelse
</div>
</div>
</x-layouts::app>
+23 -9
View File
@@ -1,13 +1,27 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Company\Http\Controllers\CompanyAddressController;
use Modules\Company\Http\Controllers\CompanyController;
use Modules\Company\Http\Controllers\CompanyDivisionController;
use Modules\Company\Http\Controllers\CompanyMembershipController;
use Modules\Company\Http\Controllers\CompanyWarehouseAssignmentController;
if (! Route::has('platform.companies.index')) {
Route::middleware(['web', 'auth', 'verified', 'tenant.assigned', 'platform.admin'])
->prefix('admin')
->name('platform.')
->group(function (): void {
Route::resource('/companies', CompanyController::class);
});
}
Route::middleware(['web', 'auth', 'verified', 'tenant.assigned', 'platform.admin'])
->prefix('admin')
->name('platform.')
->group(function () {
Route::resource('companies', CompanyController::class);
Route::put('companies/{company}/address', [CompanyAddressController::class, 'updateCompany'])->name('companies.address.update');
Route::post('companies/{company}/divisions', [CompanyDivisionController::class, 'store'])->name('companies.divisions.store');
Route::put('companies/{company}/divisions/{division}', [CompanyDivisionController::class, 'update'])->name('companies.divisions.update');
Route::delete('companies/{company}/divisions/{division}', [CompanyDivisionController::class, 'destroy'])->name('companies.divisions.destroy');
Route::put('companies/{company}/divisions/{division}/address', [CompanyAddressController::class, 'updateDivision'])->name('companies.divisions.address.update');
Route::post('companies/{company}/division-memberships', [CompanyMembershipController::class, 'assignDivision'])->name('companies.division-memberships.store');
Route::post('companies/{company}/director-assignments', [CompanyMembershipController::class, 'assignDirector'])->name('companies.director-assignments.store');
Route::delete('companies/{company}/director-assignments/{assignment}', [CompanyMembershipController::class, 'removeDirector'])->name('companies.director-assignments.destroy');
Route::put('companies/{company}/warehouses/{warehouse}/division', [CompanyWarehouseAssignmentController::class, 'update'])->name('companies.warehouses.division.update');
Route::put('companies/{company}/warehouses/{warehouse}/address', [CompanyAddressController::class, 'updateWarehouse'])->name('companies.warehouses.address.update');
});
-126
View File
@@ -1696,129 +1696,3 @@ html[data-wms-sidebar="collapsed"] .wms-sidebar {
color: rgb(191 219 254);
}
}
/* Company/country Select2 dark override -------------------------------------------------
Keep the Select2 country field visually consistent with the WMS dark form inputs. */
.wms-select2-field .select2-container,
.wms-select2-field .select2-container--wms,
.wms-select2-field .select2-container--default {
margin-top: 0.5rem !important;
width: 100% !important;
display: block !important;
}
.wms-select2-field .select2-container--wms .select2-selection--single,
.wms-select2-field .select2-container--default .select2-selection--single {
height: 3rem !important;
min-height: 3rem !important;
border: 1px solid rgb(63 63 70) !important;
border-radius: 0.75rem !important;
background: rgb(9 9 11) !important;
box-shadow: none !important;
display: flex !important;
align-items: center !important;
}
.wms-select2-field .select2-container--wms.select2-container--focus .select2-selection--single,
.wms-select2-field .select2-container--wms.select2-container--open .select2-selection--single,
.wms-select2-field .select2-container--default.select2-container--focus .select2-selection--single,
.wms-select2-field .select2-container--default.select2-container--open .select2-selection--single {
border-color: rgb(59 130 246) !important;
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.24) !important;
}
.wms-select2-field .select2-selection--single .select2-selection__rendered {
width: 100% !important;
padding-left: 1rem !important;
padding-right: 3rem !important;
color: rgb(244 244 245) !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
line-height: 3rem !important;
}
.wms-select2-field .select2-selection--single .select2-selection__placeholder {
color: rgb(161 161 170) !important;
font-weight: 500 !important;
}
.wms-select2-field .select2-selection--single .select2-selection__clear {
position: absolute !important;
right: 2.75rem !important;
top: 0.5rem !important;
width: 2rem !important;
height: 2rem !important;
color: rgb(161 161 170) !important;
font-size: 1.2rem !important;
line-height: 1.85rem !important;
text-align: center !important;
}
.wms-select2-field .select2-selection--single .select2-selection__arrow {
position: absolute !important;
top: 0.5rem !important;
right: 0.625rem !important;
width: 2rem !important;
height: 2rem !important;
border-radius: 0.625rem !important;
background: rgb(39 39 42) !important;
}
.wms-select2-field .select2-selection--single .select2-selection__arrow b {
border-color: rgb(212 212 216) transparent transparent transparent !important;
}
.select2-container--wms .select2-dropdown,
.select2-container--default .select2-dropdown {
overflow: hidden !important;
border: 1px solid rgb(63 63 70) !important;
border-radius: 0.875rem !important;
background: rgb(24 24 27) !important;
color: rgb(244 244 245) !important;
box-shadow: 0 24px 60px rgb(0 0 0 / 0.45) !important;
}
.select2-container--wms .select2-search--dropdown,
.select2-container--default .select2-search--dropdown {
padding: 0.75rem !important;
background: rgb(24 24 27) !important;
}
.select2-container--wms .select2-search--dropdown .select2-search__field,
.select2-container--default .select2-search--dropdown .select2-search__field {
height: 2.75rem !important;
border: 1px solid rgb(63 63 70) !important;
border-radius: 0.75rem !important;
background: rgb(9 9 11) !important;
color: rgb(244 244 245) !important;
padding: 0 0.875rem !important;
font-size: 0.875rem !important;
outline: none !important;
}
.select2-container--wms .select2-results__options,
.select2-container--default .select2-results__options {
background: rgb(24 24 27) !important;
max-height: 16rem !important;
}
.select2-container--wms .select2-results__option,
.select2-container--default .select2-results__option {
padding: 0.75rem 1rem !important;
color: rgb(244 244 245) !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
}
.select2-container--wms .select2-results__option--highlighted.select2-results__option--selectable,
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
background: rgb(37 99 235) !important;
color: white !important;
}
.select2-container--wms .select2-results__option--selected,
.select2-container--default .select2-results__option--selected {
background: rgb(30 41 59) !important;
color: rgb(191 219 254) !important;
}
+73 -52
View File
@@ -1,5 +1,8 @@
/* WMS Select2 professional dark theme.
This file is imported after Select2's own CSS so it wins reliably. */
/* WMS Select2 professional dark theme. */
.wms-select2-field {
min-width: 0;
}
.wms-select2-field .select2-container,
.select2-container--wms {
@@ -7,7 +10,12 @@
display: block !important;
}
.wms-select2-field select[data-wms-select2].select2-hidden-accessible {
.wms-select2-field .select2-container {
margin-top: 0.5rem !important;
}
.wms-select2-field select[data-wms-select2].select2-hidden-accessible,
.wms-select2-field select.wms-select2-control.select2-hidden-accessible {
position: absolute !important;
width: 1px !important;
height: 1px !important;
@@ -19,19 +27,14 @@
margin: -1px !important;
}
.wms-select2-field .select2-container {
margin-top: 0.5rem !important;
}
.select2-container--wms .select2-selection--single {
position: relative !important;
height: 3rem !important;
min-height: 3rem !important;
border: 1px solid rgb(63 63 70) !important;
border-radius: 0.75rem !important;
background: rgb(9 9 11) !important;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2) !important;
display: flex !important;
align-items: center !important;
outline: none !important;
transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease !important;
}
@@ -43,8 +46,8 @@
}
.select2-container--wms .select2-selection--single .select2-selection__rendered {
width: 100% !important;
padding: 0 3rem 0 1rem !important;
height: 100% !important;
padding: 0 2.75rem 0 1rem !important;
color: rgb(244 244 245) !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
@@ -56,43 +59,58 @@
font-weight: 500 !important;
}
/* Keep the Select2 arrow as a plain chevron, not a boxed button. */
.select2-container--wms .select2-selection--single .select2-selection__arrow {
position: absolute !important;
top: 0.5rem !important;
right: 0.625rem !important;
width: 2rem !important;
height: 2rem !important;
border-radius: 0.625rem !important;
background: rgb(39 39 42) !important;
top: 0 !important;
right: 0.875rem !important;
width: 1.25rem !important;
height: 3rem !important;
background: transparent !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.select2-container--wms .select2-selection--single .select2-selection__arrow b {
border-color: rgb(212 212 216) transparent transparent transparent !important;
border-width: 5px 4px 0 4px !important;
position: absolute !important;
left: 50% !important;
top: 50% !important;
width: 0 !important;
height: 0 !important;
margin-left: -4px !important;
margin-top: -2px !important;
border-style: solid !important;
border-width: 5px 4px 0 4px !important;
border-color: rgb(212 212 216) transparent transparent transparent !important;
}
.select2-container--wms.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent rgb(212 212 216) transparent !important;
margin-top: -3px !important;
border-width: 0 4px 5px 4px !important;
border-color: transparent transparent rgb(212 212 216) transparent !important;
}
.select2-container--wms .select2-selection--single .select2-selection__clear {
position: absolute !important;
right: 2.85rem !important;
top: 0.5rem !important;
width: 2rem !important;
height: 2rem !important;
border-radius: 0.625rem !important;
top: 0 !important;
right: 2.25rem !important;
height: 3rem !important;
width: 1.25rem !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
color: rgb(161 161 170) !important;
font-size: 1.25rem !important;
line-height: 1.85rem !important;
font-size: 1.1rem !important;
font-weight: 700 !important;
line-height: 3rem !important;
text-align: center !important;
}
.select2-container--wms .select2-selection--single .select2-selection__clear:hover {
background: rgb(39 39 42) !important;
color: rgb(244 244 245) !important;
}
@@ -106,21 +124,26 @@
}
.select2-container--wms .select2-search--dropdown {
padding: 0.75rem !important;
padding: 0.625rem !important;
background: rgb(24 24 27) !important;
}
.select2-container--wms .select2-search--dropdown .select2-search__field {
height: 2.75rem !important;
height: 2.5rem !important;
border: 1px solid rgb(63 63 70) !important;
border-radius: 0.75rem !important;
background: rgb(9 9 11) !important;
color: rgb(244 244 245) !important;
padding: 0 0.875rem !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
outline: none !important;
}
.select2-container--wms .select2-search--dropdown .select2-search__field::placeholder {
color: rgb(161 161 170) !important;
}
.select2-container--wms .select2-search--dropdown .select2-search__field:focus {
border-color: rgb(59 130 246) !important;
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.22) !important;
@@ -132,7 +155,7 @@
}
.select2-container--wms .select2-results__option {
padding: 0.75rem 1rem !important;
padding: 0.7rem 1rem !important;
color: rgb(244 244 245) !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
@@ -148,31 +171,29 @@
color: rgb(191 219 254) !important;
}
.select2-container--wms .select2-results__option[aria-disabled='true'] {
color: rgb(113 113 122) !important;
}
.select2-container--wms .select2-results__option[aria-disabled='true'],
.select2-container--wms .select2-results__message {
color: rgb(161 161 170) !important;
}
/* Safety: any WMS Select2 target should disappear once Select2 has initialised,
even if a view is not wrapped in .wms-select2-field. */
select[data-wms-select2].select2-hidden-accessible,
select[data-company-country-select2].select2-hidden-accessible,
select[data-helpdesk-select2].select2-hidden-accessible {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0 0 0 0) !important;
white-space: nowrap !important;
border: 0 !important;
padding: 0 !important;
margin: -1px !important;
/* Fallback styling before Select2 initialises, matching WMS inputs. */
.wms-select2-field > select[data-wms-select2],
.wms-select2-field > select.wms-select2-control {
margin-top: 0.5rem;
height: 3rem;
width: 100%;
border-radius: 0.75rem;
border: 1px solid rgb(63 63 70);
background: rgb(9 9 11);
color: rgb(244 244 245);
padding: 0 1rem;
font-size: 0.875rem;
font-weight: 500;
}
/* Keep country Select2 controls aligned with the surrounding WMS form fields. */
.wms-select2-field .select2-container--wms {
margin-top: 0.5rem !important;
.wms-select2-field > select[data-wms-select2]:focus,
.wms-select2-field > select.wms-select2-control:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.24);
outline: none;
}
+22 -8
View File
@@ -7,11 +7,13 @@ import $ from 'jquery';
window.$ = $;
window.jQuery = $;
import attachSelect2 from 'select2';
import select2Factory from 'select2';
import 'select2/dist/css/select2.min.css';
import '../css/wms-select2.css';
attachSelect2(window.jQuery);
if (typeof select2Factory === 'function') {
select2Factory(window, $);
}
const chartColours = [
'rgb(8, 145, 178)',
@@ -170,25 +172,27 @@ function initialiseSelect2() {
return;
}
$('[data-helpdesk-select2], [data-wms-select2], [data-company-country-select2]').each(function () {
$('[data-helpdesk-select2], [data-wms-select2], .wms-select2-control').each(function () {
const $select = $(this);
if ($select.hasClass('select2-hidden-accessible')) {
return;
$select.select2('destroy');
}
const isMultiple = $select.prop('multiple');
const placeholder =
$select.data('placeholder') ||
$select.find('option[value=""]').first().text() ||
(isMultiple ? 'Select options' : 'Select option');
$select.select2({
theme: 'wms',
width: '100%',
allowClear: !isMultiple,
closeOnSelect: !isMultiple,
dropdownAutoWidth: false,
placeholder:
$select.data('placeholder') ||
$select.find('option[value=""]').first().text() ||
(isMultiple ? 'Select options' : 'Select option'),
placeholder,
sorter(data) {
return data.sort((first, second) => first.text.localeCompare(second.text, undefined, {
sensitivity: 'base',
@@ -197,6 +201,16 @@ function initialiseSelect2() {
},
});
$select.off('select2:open.wms-select2');
$select.on('select2:open.wms-select2', function () {
const searchField = document.querySelector('.select2-container--open .select2-search__field');
if (searchField) {
searchField.placeholder = placeholder;
searchField.focus();
}
});
$select.off('change.select2-livewire');
$select.on('change.select2-livewire', function () {