Country / Company updates
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
<?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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
@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>
|
||||
@@ -0,0 +1,174 @@
|
||||
<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>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -18,10 +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->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ class CompanyController extends Controller
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
]),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,7 +85,8 @@ class CompanyController extends Controller
|
||||
public function edit(Company $company)
|
||||
{
|
||||
return view('company::edit', [
|
||||
'company' => $company,
|
||||
'company' => $company->loadMissing(['address.country', 'country']),
|
||||
'countries' => $this->countries(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -119,12 +122,20 @@ class CompanyController extends Controller
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'billing_email' => ['nullable', 'email', 'max:255'],
|
||||
'status' => ['required', Rule::in(['active', 'suspended', 'inactive'])],
|
||||
'timezone' => ['nullable', 'string', 'max:64'],
|
||||
'country_id' => ['nullable', Rule::exists('countries', 'id')->where('is_active', true)],
|
||||
]);
|
||||
|
||||
$data['slug'] = Str::slug($data['name']);
|
||||
$data['timezone'] = $data['timezone'] ?? ($company?->timezone ?: 'Europe/London');
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ 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
|
||||
@@ -18,7 +20,7 @@ class Company extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'uuid', 'name', 'slug', 'status', 'timezone', 'settings',
|
||||
'is_system_company', 'stripe_customer_id', 'billing_email',
|
||||
'is_system_company', 'stripe_customer_id', 'billing_email', 'country_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -37,6 +39,12 @@ class Company extends Model
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
public function address(): MorphOne
|
||||
{
|
||||
return $this->morphOne(CompanyAddress::class, 'addressable');
|
||||
|
||||
@@ -47,15 +47,23 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="timezone" class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Timezone</label>
|
||||
<input
|
||||
id="timezone"
|
||||
name="timezone"
|
||||
value="{{ old('timezone', $company->timezone ?? 'Europe/London') }}"
|
||||
<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"
|
||||
>
|
||||
@error('timezone')
|
||||
<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>
|
||||
|
||||
@@ -55,12 +55,16 @@
|
||||
<option value="inactive" @selected(old('status') === 'inactive')>Inactive</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
name="timezone"
|
||||
value="{{ old('timezone', 'Europe/London') }}"
|
||||
placeholder="Europe/London"
|
||||
class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white"
|
||||
>
|
||||
<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
|
||||
|
||||
+1531
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,222 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1409,3 +1409,123 @@ html[data-wms-sidebar="collapsed"] .wms-sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer components {
|
||||
.wms-select2-field .select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.wms-select2-field .select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(113 113 122);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-search--dropdown {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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--default .select2-results__option {
|
||||
padding: 0.65rem 0.85rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background: rgb(8 145 178);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--selected {
|
||||
background: rgb(236 254 255);
|
||||
color: rgb(14 116 144);
|
||||
}
|
||||
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
}
|
||||
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(161 161 170);
|
||||
}
|
||||
|
||||
.dark .wms-select2-field .select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
background: rgb(39 39 42);
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-dropdown {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(24 24 27);
|
||||
color: rgb(244 244 245);
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border-color: rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-results__option--selected {
|
||||
background: rgb(8 47 73);
|
||||
color: rgb(165 243 252);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -167,7 +167,7 @@ function initialiseSelect2() {
|
||||
return;
|
||||
}
|
||||
|
||||
$('[data-helpdesk-select2]').each(function () {
|
||||
$('[data-helpdesk-select2], [data-wms-select2]').each(function () {
|
||||
const $select = $(this);
|
||||
|
||||
if ($select.hasClass('select2-hidden-accessible')) {
|
||||
@@ -182,6 +182,7 @@ function initialiseSelect2() {
|
||||
closeOnSelect: !isMultiple,
|
||||
dropdownAutoWidth: true,
|
||||
placeholder:
|
||||
$select.data('placeholder') ||
|
||||
$select.find('option[value=""]').first().text() ||
|
||||
(isMultiple ? 'Select options' : 'Select option'),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user