diff --git a/Company/Database/Migrations/2026_07_07_093000_add_country_id_to_companies_table.php b/Company/Database/Migrations/2026_07_07_093000_add_country_id_to_companies_table.php new file mode 100644 index 00000000..0d0a61f0 --- /dev/null +++ b/Company/Database/Migrations/2026_07_07_093000_add_country_id_to_companies_table.php @@ -0,0 +1,26 @@ +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'); + }); + } +}; diff --git a/Company/Http/Controllers/CompanyController.php b/Company/Http/Controllers/CompanyController.php new file mode 100644 index 00000000..0880b292 --- /dev/null +++ b/Company/Http/Controllers/CompanyController.php @@ -0,0 +1,141 @@ + 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']); + } +} diff --git a/Company/Models/Company.php b/Company/Models/Company.php new file mode 100644 index 00000000..e3dd2a77 --- /dev/null +++ b/Company/Models/Company.php @@ -0,0 +1,100 @@ + '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(); + } +} diff --git a/Company/Resources/views/_form.blade.php b/Company/Resources/views/_form.blade.php new file mode 100644 index 00000000..e044d2d7 --- /dev/null +++ b/Company/Resources/views/_form.blade.php @@ -0,0 +1,83 @@ +@csrf + +
+
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('billing_email') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('status') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('country_id') +

{{ $message }}

+ @enderror +
+
+ +
+ + + + Cancel + +
diff --git a/Company/Resources/views/index.blade.php b/Company/Resources/views/index.blade.php new file mode 100644 index 00000000..b4515449 --- /dev/null +++ b/Company/Resources/views/index.blade.php @@ -0,0 +1,174 @@ + +
+ + + + + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+
+
+

Create company

+

Add a new tenant without leaving the administration index.

+
+ + + Open full form + +
+ +
+ @csrf + + + + + + + +
+ +
+ + +
+
+
+ + + + + + + + + + + + + + + @forelse ($companies as $company) + + + + + + + + + + + + + + + + @empty + + + + @endforelse + +
NameUsersDivisionsWarehousesLicencesStatusActions
+ {{ $company->name }} + + {{ $company->users_count }} + + {{ $company->divisions_count }} + + {{ $company->warehouses_count }} + + {{ $company->license_keys_count }} + + + {{ $company->is_system_company ? 'System' : ucfirst($company->status) }} + + +
+ + View + + + + Edit + + + @unless ($company->is_system_company) +
+ @csrf + @method('DELETE') + + +
+ @endunless +
+
+ No companies found. +
+
+ + {{ $companies->links() }} +
+
diff --git a/Modules/Company/Database/Migrations/2026_07_07_093000_add_country_id_to_companies_table.php b/Modules/Company/Database/Migrations/2026_07_07_093000_add_country_id_to_companies_table.php new file mode 100644 index 00000000..0d0a61f0 --- /dev/null +++ b/Modules/Company/Database/Migrations/2026_07_07_093000_add_country_id_to_companies_table.php @@ -0,0 +1,26 @@ +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'); + }); + } +}; diff --git a/Modules/Company/Http/Controllers/CompanyController.php b/Modules/Company/Http/Controllers/CompanyController.php index 5bd16514..0880b292 100644 --- a/Modules/Company/Http/Controllers/CompanyController.php +++ b/Modules/Company/Http/Controllers/CompanyController.php @@ -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']); + } } diff --git a/Modules/Company/Models/Company.php b/Modules/Company/Models/Company.php index 0a3bb73e..e3dd2a77 100644 --- a/Modules/Company/Models/Company.php +++ b/Modules/Company/Models/Company.php @@ -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'); diff --git a/Modules/Company/Resources/views/_form.blade.php b/Modules/Company/Resources/views/_form.blade.php index 5ddfb4b0..e044d2d7 100644 --- a/Modules/Company/Resources/views/_form.blade.php +++ b/Modules/Company/Resources/views/_form.blade.php @@ -47,15 +47,23 @@ @enderror -
- - + + + @error('country_id')

{{ $message }}

@enderror
diff --git a/Modules/Company/Resources/views/index.blade.php b/Modules/Company/Resources/views/index.blade.php index 8433fee2..b4515449 100644 --- a/Modules/Company/Resources/views/index.blade.php +++ b/Modules/Company/Resources/views/index.blade.php @@ -55,12 +55,16 @@ - +
+ +