interim fix for drop downs, BROKEN FIX
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
<?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('companies', function (Blueprint $table): void {
|
||||
$table->foreignId('country_id')
|
||||
->nullable()
|
||||
->after('billing_email')
|
||||
->constrained('countries')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('companies', function (Blueprint $table): void {
|
||||
$table->dropConstrainedForeignId('country_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
<?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\Countries\Models\Country;
|
||||
|
||||
class CompanyController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display companies.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('company::index', [
|
||||
'companies' => Company::query()
|
||||
->with(['address.country', 'country'])
|
||||
->withCount(['users', 'warehouses', 'licenseKeys', 'divisions'])
|
||||
->latest('id')
|
||||
->paginate(20),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the create company screen.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('company::create', [
|
||||
'company' => new Company([
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
]),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a company tenant.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$company = Company::create($this->validated($request));
|
||||
|
||||
return redirect()
|
||||
->route('platform.companies.index')
|
||||
->with('status', 'Company created.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a company workbench.
|
||||
*/
|
||||
public function show(Company $company)
|
||||
{
|
||||
$company->load([
|
||||
'address.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,
|
||||
'recentUsers' => $company->users()->latest('id')->limit(5)->get(),
|
||||
'recentWarehouses' => $company->warehouses()->latest('id')->limit(5)->get(),
|
||||
'recentLicenseKeys' => $company->licenseKeys()->latest('id')->limit(5)->get(),
|
||||
'countries' => Country::query()->active()->orderBy('sort_order')->orderBy('name')->get(['id', 'alpha2', 'name', 'display_name', 'common_name']),
|
||||
'companyUsers' => $company->users()->orderBy('name')->get(['id', 'name', 'email', 'company_id']),
|
||||
'unassignedWarehouses' => $company->warehouses()->whereNull('division_id')->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the edit company screen.
|
||||
*/
|
||||
public function edit(Company $company)
|
||||
{
|
||||
return view('company::edit', [
|
||||
'company' => $company->loadMissing(['address.country', 'country']),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a company tenant.
|
||||
*/
|
||||
public function update(Request $request, Company $company)
|
||||
{
|
||||
$company->update($this->validated($request, $company));
|
||||
|
||||
return redirect()
|
||||
->route('platform.companies.show', $company)
|
||||
->with('status', 'Company updated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a company tenant.
|
||||
*/
|
||||
public function destroy(Company $company)
|
||||
{
|
||||
abort_if($company->is_system_company, 403, 'The system company cannot be deleted.');
|
||||
|
||||
$company->delete();
|
||||
|
||||
return redirect()
|
||||
->route('platform.companies.index')
|
||||
->with('status', 'Company deleted.');
|
||||
}
|
||||
|
||||
private function validated(Request $request, ?Company $company = null): array
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'billing_email' => ['nullable', 'email', 'max:255'],
|
||||
'status' => ['required', Rule::in(['active', 'suspended', 'inactive'])],
|
||||
'country_id' => ['nullable', Rule::exists('countries', 'id')->where('is_active', true)],
|
||||
]);
|
||||
|
||||
$data['slug'] = Str::slug($data['name']);
|
||||
$data['timezone'] = $company?->timezone ?: 'Europe/London';
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function countries()
|
||||
{
|
||||
return Country::query()
|
||||
->active()
|
||||
->ordered()
|
||||
->get(['id', 'alpha2', 'name', 'display_name', 'common_name', 'sort_order']);
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
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\Warehouse\app\Models\Warehouse;
|
||||
|
||||
class Company extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'name', 'slug', 'status', 'timezone', 'settings',
|
||||
'is_system_company', 'stripe_customer_id', 'billing_email', 'country_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'settings' => 'array',
|
||||
'is_system_company' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Company $company): void {
|
||||
$company->uuid ??= (string) Str::uuid();
|
||||
$company->slug ??= Str::slug($company->name);
|
||||
$company->status ??= 'active';
|
||||
$company->timezone ??= 'Europe/London';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
public function address(): MorphOne
|
||||
{
|
||||
return $this->morphOne(CompanyAddress::class, 'addressable');
|
||||
}
|
||||
|
||||
public function divisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CompanyDivision::class);
|
||||
}
|
||||
|
||||
public function directorAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(CompanyDirectorAssignment::class);
|
||||
}
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function licenseKeys(): HasMany
|
||||
{
|
||||
return $this->hasMany(LicenseKey::class);
|
||||
}
|
||||
|
||||
public function payments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Payment::class);
|
||||
}
|
||||
|
||||
public function warehouses(): HasMany
|
||||
{
|
||||
return $this->hasMany(Warehouse::class);
|
||||
}
|
||||
|
||||
public function hasActiveLicense(): bool
|
||||
{
|
||||
if ($this->is_system_company) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->licenseKeys()
|
||||
->where('is_active', true)
|
||||
->where(function ($query): void {
|
||||
$query->whereNull('starts_at')->orWhere('starts_at', '<=', now());
|
||||
})
|
||||
->where(function ($query): void {
|
||||
$query->whereNull('expires_at')->orWhere('expires_at', '>=', now());
|
||||
})
|
||||
->whereNull('revoked_at')
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Company name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ old('name', $company->name) }}"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
required
|
||||
>
|
||||
@error('name')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="billing_email" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Billing email</label>
|
||||
<input
|
||||
id="billing_email"
|
||||
type="email"
|
||||
name="billing_email"
|
||||
value="{{ old('billing_email', $company->billing_email) }}"
|
||||
placeholder="billing@example.com"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
@error('billing_email')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Status</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
required
|
||||
>
|
||||
<option value="active" @selected(old('status', $company->status) === 'active')>Active</option>
|
||||
<option value="suspended" @selected(old('status', $company->status) === 'suspended')>Suspended</option>
|
||||
<option value="inactive" @selected(old('status', $company->status) === 'inactive')>Inactive</option>
|
||||
</select>
|
||||
@error('status')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="wms-select2-field">
|
||||
<label for="country_id" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Country</label>
|
||||
<select
|
||||
id="country_id"
|
||||
name="country_id"
|
||||
data-wms-select2
|
||||
data-placeholder="Search and select a country"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
<option value="">Search and select a country</option>
|
||||
@foreach ($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) old('country_id', $company->country_id ?? $company->address?->country_id) === $country->id)>
|
||||
{{ $country->effective_name ?? $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('country_id')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap items-center gap-3">
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-zinc-700 dark:bg-white dark:text-zinc-900">
|
||||
Save company
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="{{ $company->exists ? route('platform.companies.show', $company) : 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"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,174 +0,0 @@
|
||||
<x-layouts::app :title="__('Companies')">
|
||||
<div class="space-y-6">
|
||||
<x-wms.page-hero
|
||||
eyebrow="Platform Administration"
|
||||
title="Companies"
|
||||
description="Manage tenant companies and keep every company’s users, warehouses and records isolated."
|
||||
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="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">Create company</h2>
|
||||
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Add a new tenant without leaving the administration index.</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="{{ route('platform.companies.create') }}"
|
||||
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"
|
||||
>
|
||||
Open full form
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('platform.companies.store') }}" class="mt-5 grid gap-4 md:grid-cols-5">
|
||||
@csrf
|
||||
|
||||
<input
|
||||
name="name"
|
||||
value="{{ old('name') }}"
|
||||
placeholder="Company name"
|
||||
class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
required
|
||||
>
|
||||
|
||||
<input
|
||||
name="billing_email"
|
||||
value="{{ old('billing_email') }}"
|
||||
placeholder="billing@example.com"
|
||||
type="email"
|
||||
class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
|
||||
<select name="status" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white" required>
|
||||
<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 class="wms-select2-field">
|
||||
<select name="country_id" data-wms-select2 data-placeholder="Search country" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
<option value="">Search country</option>
|
||||
@foreach ($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) old('country_id') === $country->id)>
|
||||
{{ $country->effective_name ?? $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<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">
|
||||
<thead>
|
||||
<tr class="border-b border-zinc-200 dark:border-zinc-800">
|
||||
<th class="p-4 text-left">Name</th>
|
||||
<th class="p-4 text-left">Users</th>
|
||||
<th class="p-4 text-left">Divisions</th>
|
||||
<th class="p-4 text-left">Warehouses</th>
|
||||
<th class="p-4 text-left">Licences</th>
|
||||
<th class="p-4 text-left">Status</th>
|
||||
<th class="p-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@forelse ($companies as $company)
|
||||
<tr class="border-b border-zinc-100 dark:border-zinc-800">
|
||||
<td class="p-4 font-semibold text-zinc-900 dark:text-white">
|
||||
{{ $company->name }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-zinc-700 dark:text-zinc-300">
|
||||
{{ $company->users_count }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-zinc-700 dark:text-zinc-300">
|
||||
{{ $company->divisions_count }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-zinc-700 dark:text-zinc-300">
|
||||
{{ $company->warehouses_count }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-zinc-700 dark:text-zinc-300">
|
||||
{{ $company->license_keys_count }}
|
||||
</td>
|
||||
|
||||
<td class="p-4">
|
||||
<span class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold
|
||||
@if ($company->is_system_company)
|
||||
bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300
|
||||
@elseif ($company->status === 'active')
|
||||
bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300
|
||||
@else
|
||||
bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300
|
||||
@endif
|
||||
">
|
||||
{{ $company->is_system_company ? 'System' : ucfirst($company->status) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="p-4">
|
||||
<div class="flex justify-end gap-2">
|
||||
<a
|
||||
href="{{ route('platform.companies.show', $company) }}"
|
||||
class="rounded-lg border border-zinc-300 px-3 py-1.5 text-xs font-semibold text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{ route('platform.companies.edit', $company) }}"
|
||||
class="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-700"
|
||||
>
|
||||
Edit
|
||||
</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
|
||||
type="submit"
|
||||
class="rounded-lg bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
@endunless
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="p-8 text-center text-zinc-500 dark:text-zinc-400">
|
||||
No companies found.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ $companies->links() }}
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Company',
|
||||
'features' =>
|
||||
[
|
||||
],
|
||||
];
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
};
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
};
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CompanyDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
Model::unguard();
|
||||
|
||||
// $this->call("OthersTableSeeder");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
use Modules\Company\Support\ModuleInfo;
|
||||
|
||||
if (! function_exists('company_feature_enabled')) {
|
||||
/**
|
||||
* Check if a feature is enabled in the config.
|
||||
*/
|
||||
function company_feature_enabled(string $key): bool
|
||||
{
|
||||
return (bool) config(ModuleInfo::nameLower().".features.{$key}", false);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,11 @@ class CompanyController extends Controller
|
||||
{
|
||||
return view('company::index', [
|
||||
'companies' => Company::query()
|
||||
->with(['address.country'])
|
||||
->with(['address.country', 'country'])
|
||||
->withCount(['users', 'warehouses', 'licenseKeys', 'divisions'])
|
||||
->latest('id')
|
||||
->paginate(20),
|
||||
'countries' => $this->countriesForSelect(),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class CompanyController extends Controller
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
]),
|
||||
'countries' => $this->countriesForSelect(),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,7 @@ class CompanyController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $this->validated($request);
|
||||
|
||||
$company = Company::create($validated['company']);
|
||||
$this->syncAddressCountry($company, $validated['country_id']);
|
||||
$company = Company::create($this->validated($request));
|
||||
|
||||
return redirect()
|
||||
->route('platform.companies.index')
|
||||
@@ -76,7 +73,7 @@ class CompanyController extends Controller
|
||||
'recentUsers' => $company->users()->latest('id')->limit(5)->get(),
|
||||
'recentWarehouses' => $company->warehouses()->latest('id')->limit(5)->get(),
|
||||
'recentLicenseKeys' => $company->licenseKeys()->latest('id')->limit(5)->get(),
|
||||
'countries' => $this->countriesForSelect(),
|
||||
'countries' => Country::query()->active()->orderBy('sort_order')->orderBy('name')->get(['id', 'alpha2', 'name', 'display_name', 'common_name']),
|
||||
'companyUsers' => $company->users()->orderBy('name')->get(['id', 'name', 'email', 'company_id']),
|
||||
'unassignedWarehouses' => $company->warehouses()->whereNull('division_id')->orderBy('name')->get(),
|
||||
]);
|
||||
@@ -88,8 +85,8 @@ class CompanyController extends Controller
|
||||
public function edit(Company $company)
|
||||
{
|
||||
return view('company::edit', [
|
||||
'company' => $company->loadMissing('address.country'),
|
||||
'countries' => $this->countriesForSelect(),
|
||||
'company' => $company->loadMissing(['address.country', 'country']),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -98,10 +95,7 @@ class CompanyController extends Controller
|
||||
*/
|
||||
public function update(Request $request, Company $company)
|
||||
{
|
||||
$validated = $this->validated($request, $company);
|
||||
|
||||
$company->update($validated['company']);
|
||||
$this->syncAddressCountry($company, $validated['country_id']);
|
||||
$company->update($this->validated($request, $company));
|
||||
|
||||
return redirect()
|
||||
->route('platform.companies.show', $company)
|
||||
@@ -128,44 +122,20 @@ class CompanyController extends Controller
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'billing_email' => ['nullable', 'email', 'max:255'],
|
||||
'status' => ['required', Rule::in(['active', 'suspended', 'inactive'])],
|
||||
'country_id' => ['nullable', 'integer', Rule::exists('countries', 'id')],
|
||||
'country_id' => ['nullable', Rule::exists('countries', 'id')->where('is_active', true)],
|
||||
]);
|
||||
|
||||
return [
|
||||
'company' => [
|
||||
'name' => $data['name'],
|
||||
'billing_email' => $data['billing_email'] ?? null,
|
||||
'status' => $data['status'],
|
||||
'slug' => Str::slug($data['name']),
|
||||
'timezone' => $company?->timezone ?: 'Europe/London',
|
||||
],
|
||||
'country_id' => $data['country_id'] ?? null,
|
||||
];
|
||||
$data['slug'] = Str::slug($data['name']);
|
||||
$data['timezone'] = $company?->timezone ?: 'Europe/London';
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function countriesForSelect()
|
||||
private function countries()
|
||||
{
|
||||
return Country::query()
|
||||
->active()
|
||||
->orderByRaw("LOWER(COALESCE(NULLIF(display_name, ''), NULLIF(common_name, ''), name)) ASC")
|
||||
->get(['id', 'alpha2', 'name', 'display_name', 'common_name']);
|
||||
}
|
||||
|
||||
private function syncAddressCountry(Company $company, ?int $countryId): void
|
||||
{
|
||||
if ($countryId === null) {
|
||||
$company->address()?->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$company->address()->updateOrCreate(
|
||||
[],
|
||||
[
|
||||
'country_id' => $countryId,
|
||||
'label' => 'Registered address',
|
||||
'is_primary' => true,
|
||||
]
|
||||
);
|
||||
->ordered()
|
||||
->get(['id', 'alpha2', 'name', 'display_name', 'common_name', 'sort_order']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?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,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Models;
|
||||
|
||||
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 $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
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
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(', ');
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CompanyDirectorAssignment extends Model
|
||||
{
|
||||
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 division(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CompanyDivision::class, 'company_division_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
public function address(): MorphOne
|
||||
{
|
||||
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 directorAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(CompanyDirectorAssignment::class, 'company_division_id');
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Database\Eloquent\Factory;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\Company\Support\ModuleInfo;
|
||||
use Config;
|
||||
|
||||
class CompanyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* @var string $moduleName
|
||||
*/
|
||||
protected string $moduleName = 'Company';
|
||||
|
||||
/**
|
||||
* @var string $moduleNameLower
|
||||
*/
|
||||
protected string $moduleNameLower = 'company';
|
||||
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides():array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the application events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot() : void
|
||||
{
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->registerHelpers();
|
||||
$this->registerCommands();
|
||||
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the helpers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerHelpers(): void
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$this->publishes([
|
||||
module_path(ModuleInfo::name(), 'Config/config.php') => config_path( ModuleInfo::nameLower(). '.php'),
|
||||
], 'config');
|
||||
$this->mergeConfigFrom(
|
||||
module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register views.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/' . ModuleInfo::nameLower());
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath
|
||||
], ['views', ModuleInfo::nameLower() . '-module-views']);
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regsiter View Paths
|
||||
* @return array
|
||||
*/
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (Config::get('view.paths') as $path) {
|
||||
if (is_dir($path . '/modules/' . ModuleInfo::nameLower())) {
|
||||
$paths[] = $path . '/modules/' . ModuleInfo::nameLower();
|
||||
}
|
||||
}
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The module namespace to assume when generating URLs to actions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $moduleNamespace = '';
|
||||
// this must be blank for resource routes !!
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function map()
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
|
||||
$this->mapWebRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapWebRoutes()
|
||||
{
|
||||
Route::middleware('web')
|
||||
->namespace($this->moduleNamespace)
|
||||
->group(module_path('Company', '/Routes/web.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapApiRoutes()
|
||||
{
|
||||
Route::prefix('api')
|
||||
->middleware('api')
|
||||
->namespace($this->moduleNamespace)
|
||||
->group(module_path('Company', '/Routes/api.php'));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Company name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ old('name', $company->name) }}"
|
||||
class="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"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
required
|
||||
>
|
||||
@error('name')
|
||||
@@ -23,7 +23,7 @@
|
||||
name="billing_email"
|
||||
value="{{ old('billing_email', $company->billing_email) }}"
|
||||
placeholder="billing@example.com"
|
||||
class="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"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
@error('billing_email')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@@ -35,7 +35,7 @@
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
class="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"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
required
|
||||
>
|
||||
<option value="active" @selected(old('status', $company->status) === 'active')>Active</option>
|
||||
@@ -47,23 +47,19 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="wms-select2-field">
|
||||
<label for="country_id" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Country</label>
|
||||
<select
|
||||
id="country_id"
|
||||
name="country_id"
|
||||
data-company-country-select2
|
||||
data-placeholder="Search and select country"
|
||||
class="wms-country-select mt-2 w-full"
|
||||
data-wms-select2
|
||||
data-placeholder="Search and select a country"
|
||||
class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
<option value=""></option>
|
||||
@foreach($countries as $country)
|
||||
<option
|
||||
value="{{ $country->id }}"
|
||||
data-code="{{ $country->alpha2 }}"
|
||||
@selected(old('country_id', $company->address?->country_id) == $country->id)
|
||||
>
|
||||
{{ $country->display_name ?? $country->common_name ?? $country->name }}
|
||||
<option value="">Search and select a country</option>
|
||||
@foreach ($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) old('country_id', $company->country_id ?? $company->address?->country_id) === $country->id)>
|
||||
{{ $country->effective_name ?? $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@@ -73,189 +69,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center gap-3 border-t border-zinc-200 pt-6 dark:border-zinc-800">
|
||||
<button class="rounded-xl bg-green-600 px-5 py-2.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">
|
||||
<div class="mt-6 flex flex-wrap items-center gap-3">
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-zinc-700 dark:bg-white dark:text-zinc-900">
|
||||
Save company
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="{{ $company->exists ? route('platform.companies.show', $company) : route('platform.companies.index') }}"
|
||||
class="rounded-xl border border-zinc-300 px-5 py-2.5 text-sm font-semibold text-zinc-700 transition hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@once
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
|
||||
|
||||
<style>
|
||||
.wms-country-select.select2-hidden-accessible {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single {
|
||||
height: 3rem !important;
|
||||
min-height: 3rem !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
border-radius: 0.75rem !important;
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
box-shadow: none !important;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__rendered {
|
||||
width: 100% !important;
|
||||
height: 3rem !important;
|
||||
line-height: 3rem !important;
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 2.75rem !important;
|
||||
color: rgb(244 244 245) !important;
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(161 161 170) !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__arrow {
|
||||
height: 3rem !important;
|
||||
right: 0.75rem !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: rgb(161 161 170) transparent transparent transparent !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__clear {
|
||||
margin-right: 0.5rem !important;
|
||||
color: rgb(161 161 170) !important;
|
||||
font-size: 1rem !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container.select2-container--focus .select2-selection--single,
|
||||
.wms-country-select + .select2-container.select2-container--open .select2-selection--single {
|
||||
border-color: rgb(59 130 246) !important;
|
||||
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.28) !important;
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown.wms-country-select2-dropdown,
|
||||
.wms-country-select2-dropdown {
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
border-radius: 0.875rem !important;
|
||||
overflow: hidden !important;
|
||||
background: rgb(24 24 27) !important;
|
||||
box-shadow: 0 20px 35px -15px rgb(0 0 0 / 0.7) !important;
|
||||
}
|
||||
|
||||
.wms-country-select2-dropdown .select2-search--dropdown {
|
||||
padding: 0.75rem !important;
|
||||
border-bottom: 1px solid rgb(63 63 70) !important;
|
||||
background: rgb(24 24 27) !important;
|
||||
}
|
||||
|
||||
.wms-country-select2-dropdown .select2-search__field {
|
||||
min-height: 2.5rem !important;
|
||||
border-radius: 0.75rem !important;
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
color: rgb(244 244 245) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.wms-country-select2-dropdown .select2-results__option {
|
||||
padding: 0.7rem 0.9rem !important;
|
||||
color: rgb(228 228 231) !important;
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.wms-country-select2-dropdown .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background: rgb(37 99 235) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.wms-country-select2-dropdown .select2-results__option--selected {
|
||||
background: rgb(22 101 52) !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function loadScript(src, callback) {
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
if (existing) {
|
||||
existing.addEventListener('load', callback, { once: true });
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.defer = true;
|
||||
script.onload = callback;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function initCompanyCountrySelect2() {
|
||||
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.select2) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.jQuery('[data-company-country-select2]').each(function () {
|
||||
var select = window.jQuery(this);
|
||||
|
||||
if (select.hasClass('select2-hidden-accessible')) {
|
||||
return;
|
||||
}
|
||||
|
||||
select.select2({
|
||||
width: '100%',
|
||||
placeholder: select.data('placeholder') || 'Search and select country',
|
||||
allowClear: true,
|
||||
dropdownAutoWidth: false,
|
||||
dropdownCssClass: 'wms-country-select2-dropdown',
|
||||
selectionCssClass: 'wms-country-select2-selection'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.initialiseCompanyCountrySelect2 = initCompanyCountrySelect2;
|
||||
|
||||
function bootCompanyCountrySelect2() {
|
||||
if (window.jQuery && window.jQuery.fn && window.jQuery.fn.select2) {
|
||||
initCompanyCountrySelect2();
|
||||
return;
|
||||
}
|
||||
|
||||
loadScript('https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js', function () {
|
||||
loadScript('https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', initCompanyCountrySelect2);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bootCompanyCountrySelect2);
|
||||
document.addEventListener('livewire:navigated', bootCompanyCountrySelect2);
|
||||
bootCompanyCountrySelect2();
|
||||
})();
|
||||
</script>
|
||||
@endonce
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<x-layouts::app :title="__('Create Company')">
|
||||
<div class="space-y-6">
|
||||
<x-wms.page-hero
|
||||
eyebrow="Platform Administration"
|
||||
title="Create company"
|
||||
description="Add a new tenant company and prepare it for users, warehouses and licences."
|
||||
icon="fa-solid fa-building-circle-plus"
|
||||
/>
|
||||
|
||||
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<form method="POST" action="{{ route('platform.companies.store') }}">
|
||||
@include('company::_form')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
@@ -1,17 +0,0 @@
|
||||
<x-layouts::app :title="__('Edit Company')">
|
||||
<div class="space-y-6">
|
||||
<x-wms.page-hero
|
||||
eyebrow="Platform Administration"
|
||||
title="Edit company"
|
||||
description="Update tenant company details, status and billing contact."
|
||||
icon="fa-solid fa-building"
|
||||
/>
|
||||
|
||||
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<form method="POST" action="{{ route('platform.companies.update', $company) }}">
|
||||
@method('PUT')
|
||||
@include('company::_form')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
@@ -30,86 +30,45 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('platform.companies.store') }}" class="mt-6">
|
||||
<form method="POST" action="{{ route('platform.companies.store') }}" class="mt-5 grid gap-4 md:grid-cols-5">
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-5 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,1.15fr)_minmax(12rem,.75fr)_minmax(18rem,1.15fr)_auto] xl:items-end">
|
||||
<div>
|
||||
<label for="quick_company_name" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Company</label>
|
||||
<input
|
||||
id="quick_company_name"
|
||||
name="name"
|
||||
value="{{ old('name') }}"
|
||||
placeholder="Company name"
|
||||
class="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"
|
||||
required
|
||||
>
|
||||
@error('name')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<input
|
||||
name="name"
|
||||
value="{{ old('name') }}"
|
||||
placeholder="Company name"
|
||||
class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
required
|
||||
>
|
||||
|
||||
<div>
|
||||
<label for="quick_billing_email" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Billing email</label>
|
||||
<input
|
||||
id="quick_billing_email"
|
||||
name="billing_email"
|
||||
value="{{ old('billing_email') }}"
|
||||
placeholder="billing@example.com"
|
||||
type="email"
|
||||
class="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"
|
||||
>
|
||||
@error('billing_email')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<input
|
||||
name="billing_email"
|
||||
value="{{ old('billing_email') }}"
|
||||
placeholder="billing@example.com"
|
||||
type="email"
|
||||
class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
|
||||
<div>
|
||||
<label for="quick_status" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Status</label>
|
||||
<select
|
||||
id="quick_status"
|
||||
name="status"
|
||||
class="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"
|
||||
required
|
||||
>
|
||||
<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>
|
||||
@error('status')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<select name="status" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white" required>
|
||||
<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>
|
||||
<label for="quick_country_id" class="block text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Country</label>
|
||||
<select
|
||||
id="quick_country_id"
|
||||
name="country_id"
|
||||
data-company-country-select2
|
||||
data-placeholder="Search and select country"
|
||||
class="wms-country-select mt-2 w-full"
|
||||
>
|
||||
<option value=""></option>
|
||||
@foreach($countries as $country)
|
||||
<option
|
||||
value="{{ $country->id }}"
|
||||
data-code="{{ $country->alpha2 }}"
|
||||
@selected(old('country_id') == $country->id)
|
||||
>
|
||||
{{ $country->display_name ?? $country->common_name ?? $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('country_id')
|
||||
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button class="h-12 rounded-xl bg-green-600 px-6 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
|
||||
</button>
|
||||
<div class="wms-select2-field">
|
||||
<select name="country_id" data-wms-select2 data-placeholder="Search country" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
<option value="">Search country</option>
|
||||
@foreach ($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) old('country_id') === $country->id)>
|
||||
{{ $country->effective_name ?? $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
@@ -212,30 +171,4 @@
|
||||
|
||||
{{ $companies->links() }}
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
$('[data-company-country-select2]').select2({
|
||||
|
||||
width: '100%',
|
||||
|
||||
placeholder: 'Search and select country',
|
||||
|
||||
allowClear: true,
|
||||
|
||||
dropdownAutoWidth: false,
|
||||
|
||||
sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@endpush
|
||||
</x-layouts::app>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
@php($address = $address ?? null)
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Label</label>
|
||||
<input name="label" value="{{ old('label', $address?->label ?? $defaultLabel ?? 'Registered office') }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Attention</label>
|
||||
<input name="attention" value="{{ old('attention', $address?->attention) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Address line 1</label>
|
||||
<input name="line_1" value="{{ old('line_1', $address?->line_1) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Address line 2</label>
|
||||
<input name="line_2" value="{{ old('line_2', $address?->line_2) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Address line 3</label>
|
||||
<input name="line_3" value="{{ old('line_3', $address?->line_3) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Town / City</label>
|
||||
<input name="locality" value="{{ old('locality', $address?->locality) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">County / State / Region</label>
|
||||
<input name="administrative_area" value="{{ old('administrative_area', $address?->administrative_area) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Postcode / ZIP</label>
|
||||
<input name="postal_code" value="{{ old('postal_code', $address?->postal_code) }}" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Country</label>
|
||||
<select name="country_id" class="mt-2 w-full rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white" required>
|
||||
<option value="">Select country</option>
|
||||
@foreach ($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) old('country_id', $address?->country_id) === $country->id)>{{ $country->effective_name ?? $country->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 text-sm font-semibold text-white dark:bg-white dark:text-zinc-900">Save address</button>
|
||||
</div>
|
||||
@@ -1,140 +0,0 @@
|
||||
<x-layouts::app :title="$company->name">
|
||||
<div class="space-y-6">
|
||||
<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" />
|
||||
|
||||
<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-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">Slug</dt><dd class="font-semibold">{{ $company->slug }}</dd></div>
|
||||
</dl>
|
||||
</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">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">
|
||||
<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="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 grid gap-4 md:grid-cols-2">
|
||||
@csrf
|
||||
<input name="name" placeholder="Division name" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
|
||||
<input name="code" placeholder="Optional code" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
|
||||
<select name="status" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option value="active">Active</option><option value="suspended">Suspended</option><option value="inactive">Inactive</option></select>
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Create division</button>
|
||||
<textarea name="description" placeholder="Description" class="md:col-span-2 rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"></textarea>
|
||||
</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 grid gap-4 md:grid-cols-2">
|
||||
@csrf
|
||||
<select name="user_id" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required><option value="">Select user</option>@foreach($companyUsers as $user)<option value="{{ $user->id }}">{{ $user->name }}</option>@endforeach</select>
|
||||
<select name="coverage_type" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option value="company">Whole company</option><option value="division">Specific division</option></select>
|
||||
<select name="company_division_id" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option value="">No division</option>@foreach($company->divisions as $division)<option value="{{ $division->id }}">{{ $division->name }}</option>@endforeach</select>
|
||||
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Assign director</button>
|
||||
</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 grid gap-4 md:grid-cols-4">@csrf @method('PUT')<input name="name" value="{{ $division->name }}" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required><input name="code" value="{{ $division->code }}" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><select name="status" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><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><button class="rounded-xl bg-blue-600 px-4 py-2 font-semibold text-white">Update division</button><textarea name="description" class="md:col-span-4 rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">{{ $division->description }}</textarea></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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register API routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider within a group which
|
||||
| is assigned the "api" middleware group. Enjoy building your API!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware('auth:api')->get('/company', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
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');
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Support;
|
||||
|
||||
class ModuleInfo
|
||||
{
|
||||
protected static string $moduleName = 'Company';
|
||||
|
||||
/**
|
||||
* defines the Module Name
|
||||
* @return string
|
||||
*/
|
||||
public static function name(): string
|
||||
{
|
||||
return static::$moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* define the lowercase version
|
||||
* @return string
|
||||
*/
|
||||
public static function nameLower(): string
|
||||
{
|
||||
return strtolower(static::$moduleName);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Tests\Browser;
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Company\Models\Company;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class CompanyAdministrationBrowserTest extends DuskTestCase
|
||||
{
|
||||
public function test_platform_admin_can_see_company_address_divisions_and_director_sections(): void
|
||||
{
|
||||
$system = Company::query()->firstOrCreate(['slug' => 'mpba-limited'], ['name' => 'MPBA Limited', 'is_system_company' => true, 'status' => 'active']);
|
||||
$tenant = Company::query()->firstOrCreate(['slug' => 'sentinel'], ['name' => 'Sentinel', 'status' => 'active']);
|
||||
Country::query()->firstOrCreate(['alpha2' => 'GB'], ['alpha3' => 'GBR', 'numeric' => '826', 'name' => 'United Kingdom', 'is_active' => true]);
|
||||
$admin = User::query()->firstOrCreate(['email' => 'company-admin-browser@example.test'], [
|
||||
'company_id' => $system->id,
|
||||
'name' => 'Company Browser Admin',
|
||||
'password' => bcrypt('password'),
|
||||
'is_platform_admin' => true,
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($admin, $tenant): void {
|
||||
$browser->loginAs($admin)
|
||||
->visitRoute('platform.companies.show', $tenant)
|
||||
->assertSee('Company address')
|
||||
->assertSee('Create division')
|
||||
->assertSee('Director coverage')
|
||||
->assertSee('Warehouse division and address alignment')
|
||||
->assertSee('Divisions');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Company\Models\Company;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Modules\Warehouse\app\Models\Warehouse;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CompanyAdministrationRoutesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_platform_admin_can_update_company_address_create_division_and_assign_director(): void
|
||||
{
|
||||
$system = Company::create(['name' => 'MPBA Limited', 'is_system_company' => true]);
|
||||
$tenant = Company::create(['name' => 'Sentinel', 'billing_email' => 'sentinel@example.test']);
|
||||
$country = Country::create(['alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826', 'name' => 'United Kingdom', 'is_active' => true]);
|
||||
$admin = User::create(['company_id' => $system->id, 'name' => 'Platform Admin', 'email' => 'admin@example.test', 'password' => 'password', 'is_platform_admin' => true]);
|
||||
$director = User::create(['company_id' => $tenant->id, 'name' => 'Division Director', 'email' => 'director@example.test', 'password' => 'password']);
|
||||
|
||||
$this->actingAs($admin)->put(route('platform.companies.address.update', $tenant), [
|
||||
'line_1' => '1 Example Street',
|
||||
'locality' => 'London',
|
||||
'postal_code' => 'SW1A 1AA',
|
||||
'country_id' => $country->id,
|
||||
])->assertRedirect(route('platform.companies.show', $tenant));
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.companies.divisions.store', $tenant), [
|
||||
'name' => 'Northern Operations',
|
||||
'code' => 'NORTH',
|
||||
'status' => 'active',
|
||||
])->assertRedirect(route('platform.companies.show', $tenant));
|
||||
|
||||
$division = $tenant->divisions()->where('code', 'NORTH')->firstOrFail();
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.companies.director-assignments.store', $tenant), [
|
||||
'user_id' => $director->id,
|
||||
'coverage_type' => 'division',
|
||||
'company_division_id' => $division->id,
|
||||
])->assertRedirect(route('platform.companies.show', $tenant));
|
||||
|
||||
$this->assertDatabaseHas('company_addresses', ['line_1' => '1 Example Street', 'country_id' => $country->id]);
|
||||
$this->assertDatabaseHas('company_divisions', ['company_id' => $tenant->id, 'code' => 'NORTH']);
|
||||
$this->assertDatabaseHas('company_director_assignments', ['company_id' => $tenant->id, 'company_division_id' => $division->id, 'user_id' => $director->id]);
|
||||
}
|
||||
public function test_platform_admin_can_update_warehouse_address(): void
|
||||
{
|
||||
$system = Company::create(['name' => 'MPBA Limited', 'is_system_company' => true]);
|
||||
$tenant = Company::create(['name' => 'Sentinel', 'billing_email' => 'sentinel@example.test']);
|
||||
$country = Country::create(['alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826', 'name' => 'United Kingdom', 'is_active' => true]);
|
||||
$warehouse = Warehouse::create(['company_id' => $tenant->id, 'code' => 'MAIN', 'name' => 'Main Warehouse']);
|
||||
$admin = User::create(['company_id' => $system->id, 'name' => 'Platform Admin', 'email' => 'warehouse-admin@example.test', 'password' => 'password', 'is_platform_admin' => true]);
|
||||
|
||||
$this->actingAs($admin)->put(route('platform.companies.warehouses.address.update', [$tenant, $warehouse]), [
|
||||
'label' => 'Warehouse address',
|
||||
'line_1' => '100 Distribution Park',
|
||||
'locality' => 'Birmingham',
|
||||
'postal_code' => 'B1 1AA',
|
||||
'country_id' => $country->id,
|
||||
])->assertRedirect(route('platform.companies.show', $tenant));
|
||||
|
||||
$this->assertDatabaseHas('company_addresses', [
|
||||
'addressable_type' => Warehouse::class,
|
||||
'addressable_id' => $warehouse->id,
|
||||
'line_1' => '100 Distribution Park',
|
||||
'country_id' => $country->id,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Company\Models\Company;
|
||||
use Modules\Company\Models\CompanyAddress;
|
||||
use Modules\Company\Models\CompanyDirectorAssignment;
|
||||
use Modules\Company\Models\CompanyDivision;
|
||||
use Modules\Countries\Models\Country;
|
||||
use Modules\Warehouse\app\Models\Warehouse;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CompanyStructureTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_company_has_one_editable_country_backed_address(): void
|
||||
{
|
||||
$company = Company::create(['name' => 'Abbott Logistics Limited', 'billing_email' => 'billing@abbott.example']);
|
||||
$country = Country::create(['alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826', 'name' => 'United Kingdom', 'is_active' => true]);
|
||||
|
||||
$company->address()->create([
|
||||
'line_1' => '1 Warehouse Way',
|
||||
'locality' => 'London',
|
||||
'postal_code' => 'SW1A 1AA',
|
||||
'country_id' => $country->id,
|
||||
]);
|
||||
|
||||
$company->address()->update(['line_1' => '2 Warehouse Way']);
|
||||
|
||||
$this->assertSame(1, CompanyAddress::count());
|
||||
$this->assertSame('2 Warehouse Way', $company->fresh()->address->line_1);
|
||||
$this->assertSame('United Kingdom', $company->fresh()->address->country->name);
|
||||
}
|
||||
|
||||
public function test_company_division_has_address_users_directors_and_optional_warehouses(): void
|
||||
{
|
||||
$company = Company::create(['name' => 'Sentinel', 'billing_email' => 'sentinel@example.test']);
|
||||
$country = Country::create(['alpha2' => 'GB', 'alpha3' => 'GBR', 'numeric' => '826', 'name' => 'United Kingdom', 'is_active' => true]);
|
||||
$user = User::create(['company_id' => $company->id, 'name' => 'Jane Director', 'email' => 'jane@example.test', 'password' => 'password']);
|
||||
$division = $company->divisions()->create(['name' => 'North Operations', 'code' => 'NORTH']);
|
||||
$warehouse = Warehouse::create(['company_id' => $company->id, 'division_id' => $division->id, 'code' => 'NTH', 'name' => 'North DC']);
|
||||
|
||||
$division->address()->create([
|
||||
'label' => 'Operating address',
|
||||
'line_1' => 'North Road',
|
||||
'locality' => 'Manchester',
|
||||
'country_id' => $country->id,
|
||||
]);
|
||||
$division->users()->attach($user->id, ['company_id' => $company->id, 'role' => 'director', 'is_director' => true]);
|
||||
CompanyDirectorAssignment::create(['company_id' => $company->id, 'company_division_id' => $division->id, 'user_id' => $user->id, 'coverage_type' => 'division']);
|
||||
|
||||
$this->assertTrue($company->fresh()->divisions()->exists());
|
||||
$this->assertSame('North Operations', $warehouse->fresh()->division->name);
|
||||
$this->assertSame('Manchester', $division->fresh()->address->locality);
|
||||
$this->assertTrue($user->fresh()->divisions()->whereKey($division->id)->exists());
|
||||
$this->assertTrue($user->fresh()->directorAssignments()->where('company_division_id', $division->id)->exists());
|
||||
}
|
||||
|
||||
public function test_warehouse_can_remain_at_company_level_without_division(): void
|
||||
{
|
||||
$company = Company::create(['name' => 'Company Level Warehousing']);
|
||||
$warehouse = Warehouse::create(['company_id' => $company->id, 'code' => 'MAIN', 'name' => 'Main Warehouse']);
|
||||
|
||||
$this->assertNull($warehouse->division_id);
|
||||
$this->assertSame('Company Level Warehousing', $warehouse->company->name);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
latest: 1.2.1
|
||||
|
||||
versions:
|
||||
- version: 1.2.1
|
||||
date: 2026-07-07
|
||||
changes:
|
||||
- Replaced company country inputs with a shared Select2-enhanced dropdown populated by the Countries module.
|
||||
- Aligned the company create forms and changed the primary create/save action to green.
|
||||
- Stored the selected country on the company address relationship during create and update.
|
||||
- version: 1.2.0
|
||||
date: 2026-07-02
|
||||
changes:
|
||||
- Added one-address-per-owner company address records using an industry-standard address layout backed by the Countries module.
|
||||
- Added company divisions with optional codes, status, soft deletes, one address per division, user memberships and director coverage.
|
||||
- Added optional division assignment and proper address relationship support for warehouses.
|
||||
- Added Company module migrations with foreign keys, unique constraints and performance indexes.
|
||||
- Added feature and Dusk coverage for company addresses, divisions, director coverage and warehouse division behaviour.
|
||||
- version: 1.1.0
|
||||
date: 2026-07-02
|
||||
changes:
|
||||
- Added full company create, show, edit, update and delete workflow.
|
||||
- Added company workbench show page with profile, KPI cards, recent users, warehouses and licences.
|
||||
- Added guarded deletion so the system company cannot be deleted.
|
||||
- Standardised Company module views on x-layouts::app and company::_form includes.
|
||||
- version: 1.0.2
|
||||
date: 2026-07-02
|
||||
changes:
|
||||
- Fixed company create/update validation when timezone is omitted from request payloads.
|
||||
- version: 1.0.0
|
||||
date: 2026-07-02
|
||||
changes:
|
||||
- Initial Company module skeleton.
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "mpba/company",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Gregory",
|
||||
"email": "mark.gregory@mpba.co.uk"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Company\\": ""
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/",
|
||||
"Modules\\Company\\Tests\\": "modules/Company/Tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "Company",
|
||||
"alias": "company",
|
||||
"version": "1.2.1",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Company\\Providers\\CompanyServiceProvider"
|
||||
],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": []
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run development",
|
||||
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch-poll": "npm run watch -- --watch-poll",
|
||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"prod": "npm run production",
|
||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0",
|
||||
"laravel-mix": "^5.0.1",
|
||||
"laravel-mix-merge-manifest": "^0.1.2"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
const dotenvExpand = require('dotenv-expand');
|
||||
dotenvExpand(require('dotenv').config({ path: '../../.env'/*, debug: true*/}));
|
||||
|
||||
const mix = require('laravel-mix');
|
||||
|
||||
mix.js(__dirname + '/Resources/assets/js/app.js', 'js/company.js')
|
||||
.sass( __dirname + '/Resources/assets/sass/app.scss', 'css/company.css');
|
||||
|
||||
if (mix.inProduction()) {
|
||||
mix.version();
|
||||
}
|
||||
-1531
File diff suppressed because it is too large
Load Diff
@@ -1,222 +0,0 @@
|
||||
require('flatpickr');
|
||||
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
import Chart from 'chart.js/auto';
|
||||
import $ from 'jquery';
|
||||
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
|
||||
import 'select2';
|
||||
import 'select2/dist/css/select2.min.css';
|
||||
|
||||
const chartColours = [
|
||||
'rgb(8, 145, 178)',
|
||||
'rgb(37, 99, 235)',
|
||||
'rgb(79, 70, 229)',
|
||||
'rgb(14, 116, 144)',
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse JSON attribute safely.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} attribute
|
||||
* @param {Array} fallback
|
||||
* @returns {*}
|
||||
*/
|
||||
function parseJsonAttribute(element, attribute, fallback = []) {
|
||||
try {
|
||||
return JSON.parse(
|
||||
element.getAttribute(attribute) || JSON.stringify(fallback)
|
||||
);
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dashboard charts.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function renderWmsCharts() {
|
||||
document.querySelectorAll('[data-wms-chart]').forEach((canvas) => {
|
||||
if (canvas.dataset.rendered === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = canvas.getAttribute('data-wms-chart');
|
||||
const labels = parseJsonAttribute(canvas, 'data-labels');
|
||||
const series = parseJsonAttribute(canvas, 'data-series');
|
||||
const values = parseJsonAttribute(canvas, 'data-values');
|
||||
|
||||
const datasets = series.length
|
||||
? series.map((item, index) => ({
|
||||
label: item.label,
|
||||
data: item.data,
|
||||
backgroundColor:
|
||||
chartColours[index % chartColours.length],
|
||||
borderColor:
|
||||
chartColours[index % chartColours.length],
|
||||
borderWidth: 2,
|
||||
borderRadius: 8,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
label: 'Total',
|
||||
data: values,
|
||||
backgroundColor: chartColours,
|
||||
borderColor: 'rgb(255,255,255)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
];
|
||||
|
||||
new Chart(canvas, {
|
||||
type,
|
||||
data: {
|
||||
labels,
|
||||
datasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: document.documentElement.classList.contains(
|
||||
'dark'
|
||||
)
|
||||
? '#e4e4e7'
|
||||
: '#3f3f46',
|
||||
boxWidth: 12,
|
||||
boxHeight: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales:
|
||||
type === 'doughnut'
|
||||
? {}
|
||||
: {
|
||||
x: {
|
||||
ticks: {
|
||||
color:
|
||||
document.documentElement.classList.contains(
|
||||
'dark'
|
||||
)
|
||||
? '#d4d4d8'
|
||||
: '#52525b',
|
||||
},
|
||||
grid: {
|
||||
color:
|
||||
'rgba(113,113,122,0.18)',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
color:
|
||||
document.documentElement.classList.contains(
|
||||
'dark'
|
||||
)
|
||||
? '#d4d4d8'
|
||||
: '#52525b',
|
||||
},
|
||||
grid: {
|
||||
color:
|
||||
'rgba(113,113,122,0.18)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
canvas.dataset.rendered = 'true';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function livewireComponentFor(element) {
|
||||
const componentRoot = element.closest('[wire\:id]');
|
||||
|
||||
if (!componentRoot || !window.Livewire) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.Livewire.find(componentRoot.getAttribute('wire:id'));
|
||||
}
|
||||
|
||||
function setLivewireProperty(element, model, value) {
|
||||
const component = livewireComponentFor(element);
|
||||
|
||||
if (!component || !model) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.set(model, value === '' ? null : value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise Select2 controls.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function initialiseSelect2() {
|
||||
if (!window.jQuery?.fn?.select2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('[data-helpdesk-select2], [data-wms-select2]').each(function () {
|
||||
const $select = $(this);
|
||||
|
||||
if ($select.hasClass('select2-hidden-accessible')) {
|
||||
$select.select2('destroy');
|
||||
}
|
||||
|
||||
const isMultiple = $select.prop('multiple');
|
||||
|
||||
$select.select2({
|
||||
width: '100%',
|
||||
allowClear: !isMultiple,
|
||||
closeOnSelect: !isMultiple,
|
||||
dropdownAutoWidth: true,
|
||||
placeholder:
|
||||
$select.data('placeholder') ||
|
||||
$select.find('option[value=""]').first().text() ||
|
||||
(isMultiple ? 'Select options' : 'Select option'),
|
||||
});
|
||||
|
||||
$select.off('change.select2-livewire');
|
||||
|
||||
$select.on('change.select2-livewire', function () {
|
||||
setLivewireProperty(this, this.dataset.wireModel, $select.val());
|
||||
|
||||
this.dispatchEvent(
|
||||
new Event('change', {
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot page UI.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function bootUi() {
|
||||
renderWmsCharts();
|
||||
initialiseSelect2();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bootUi);
|
||||
|
||||
document.addEventListener('livewire:navigated', bootUi);
|
||||
|
||||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.hook('morphed', () => {
|
||||
initialiseSelect2();
|
||||
});
|
||||
});
|
||||
+98
-106
@@ -1410,130 +1410,122 @@ html[data-wms-sidebar="collapsed"] .wms-sidebar {
|
||||
}
|
||||
|
||||
|
||||
/* Company country Select2 */
|
||||
.wms-country-select + .select2-container {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single {
|
||||
height: 3rem;
|
||||
min-height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgb(212 212 216);
|
||||
background: rgb(255 255 255);
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
|
||||
}
|
||||
@layer components {
|
||||
.wms-select2-field .select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.dark .wms-country-select + .select2-container .select2-selection--single {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
}
|
||||
.wms-select2-field .select2-container--default .select2-selection--single {
|
||||
min-height: 2.875rem;
|
||||
border-radius: 0.75rem;
|
||||
border-color: rgb(212 212 216);
|
||||
background: white;
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
|
||||
transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__rendered {
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 2.75rem;
|
||||
color: rgb(24 24 27);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.wms-select2-field .select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
padding-left: 0.95rem;
|
||||
padding-right: 2.75rem;
|
||||
color: rgb(24 24 27);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 2.875rem;
|
||||
}
|
||||
|
||||
.dark .wms-country-select + .select2-container .select2-selection--single .select2-selection__rendered {
|
||||
color: rgb(244 244 245);
|
||||
}
|
||||
.wms-select2-field .select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(113 113 122);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(113 113 122);
|
||||
}
|
||||
.wms-select2-field .select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
top: 0.45rem;
|
||||
right: 0.65rem;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 0.55rem;
|
||||
background: rgb(244 244 245);
|
||||
}
|
||||
|
||||
.dark .wms-country-select + .select2-container .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(161 161 170);
|
||||
}
|
||||
.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(8 145 178);
|
||||
box-shadow: 0 0 0 3px rgb(103 232 249 / 0.28);
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__arrow {
|
||||
height: 3rem;
|
||||
right: 0.75rem;
|
||||
top: 0;
|
||||
}
|
||||
.select2-container--default .select2-dropdown {
|
||||
overflow: hidden;
|
||||
border-radius: 1rem;
|
||||
border-color: rgb(212 212 216);
|
||||
background: white;
|
||||
box-shadow: 0 24px 60px rgb(15 23 42 / 0.18);
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: rgb(113 113 122) transparent transparent transparent;
|
||||
}
|
||||
.select2-container--default .select2-search--dropdown {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container .select2-selection--single .select2-selection__clear {
|
||||
order: 2;
|
||||
margin-right: 0.5rem;
|
||||
color: rgb(113 113 122);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border-radius: 0.75rem;
|
||||
border-color: rgb(212 212 216);
|
||||
padding: 0.55rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wms-country-select + .select2-container.select2-container--focus .select2-selection--single,
|
||||
.wms-country-select + .select2-container.select2-container--open .select2-selection--single {
|
||||
border-color: rgb(59 130 246);
|
||||
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.28);
|
||||
}
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field:focus {
|
||||
border-color: rgb(8 145 178);
|
||||
box-shadow: 0 0 0 3px rgb(103 232 249 / 0.28);
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown {
|
||||
border: 1px solid rgb(63 63 70);
|
||||
border-radius: 0.875rem;
|
||||
overflow: hidden;
|
||||
background: rgb(255 255 255);
|
||||
box-shadow: 0 20px 35px -15px rgb(0 0 0 / 0.45);
|
||||
}
|
||||
.select2-container--default .select2-results__option {
|
||||
padding: 0.65rem 0.85rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .select2-container--open .select2-dropdown {
|
||||
background: rgb(24 24 27);
|
||||
border-color: rgb(63 63 70);
|
||||
}
|
||||
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background: rgb(8 145 178);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-search--dropdown {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid rgb(228 228 231);
|
||||
}
|
||||
.select2-container--default .select2-results__option--selected {
|
||||
background: rgb(236 254 255);
|
||||
color: rgb(14 116 144);
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-search--dropdown {
|
||||
border-bottom-color: rgb(63 63 70);
|
||||
}
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
min-height: 2.5rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgb(212 212 216);
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: rgb(24 24 27);
|
||||
outline: none;
|
||||
}
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
color: rgb(244 244 245);
|
||||
}
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(161 161 170);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option {
|
||||
padding: 0.7rem 0.9rem;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(39 39 42);
|
||||
}
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
background: rgb(39 39 42);
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-results__option {
|
||||
color: rgb(228 228 231);
|
||||
}
|
||||
.dark .select2-container--default .select2-dropdown {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(24 24 27);
|
||||
color: rgb(244 244 245);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable,
|
||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background: rgb(37 99 235);
|
||||
color: white;
|
||||
}
|
||||
.dark .select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--selected,
|
||||
.select2-container--default .select2-results__option[aria-selected="true"] {
|
||||
background: rgb(22 101 52);
|
||||
color: white;
|
||||
.dark .select2-container--default .select2-results__option--selected {
|
||||
background: rgb(8 47 73);
|
||||
color: rgb(165 243 252);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-8
@@ -167,7 +167,7 @@ function initialiseSelect2() {
|
||||
return;
|
||||
}
|
||||
|
||||
$('[data-helpdesk-select2], [data-company-country-select2]').each(function () {
|
||||
$('[data-helpdesk-select2], [data-wms-select2]').each(function () {
|
||||
const $select = $(this);
|
||||
|
||||
if ($select.hasClass('select2-hidden-accessible')) {
|
||||
@@ -176,17 +176,15 @@ function initialiseSelect2() {
|
||||
|
||||
const isMultiple = $select.prop('multiple');
|
||||
|
||||
const placeholder =
|
||||
$select.data('placeholder') ||
|
||||
$select.find('option[value=""]').first().text() ||
|
||||
(isMultiple ? 'Select options' : 'Select option');
|
||||
|
||||
$select.select2({
|
||||
width: '100%',
|
||||
allowClear: !isMultiple,
|
||||
closeOnSelect: !isMultiple,
|
||||
dropdownAutoWidth: false,
|
||||
placeholder,
|
||||
dropdownAutoWidth: true,
|
||||
placeholder:
|
||||
$select.data('placeholder') ||
|
||||
$select.find('option[value=""]').first().text() ||
|
||||
(isMultiple ? 'Select options' : 'Select option'),
|
||||
});
|
||||
|
||||
$select.off('change.select2-livewire');
|
||||
|
||||
Reference in New Issue
Block a user