php-commerce/View/Modal/Admin/EntityManager.php
Snider a774f4e285 refactor: migrate namespace from Core\Commerce to Core\Mod\Commerce
Align commerce module with the monorepo module structure by updating
all namespaces to use the Core\Mod\Commerce convention. This change
supports the recent monorepo separation and ensures consistency with
other modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:23:12 +00:00

316 lines
9.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Admin;
use Core\Mod\Commerce\Models\Entity;
use Core\Mod\Tenant\Models\Workspace;
use Livewire\Attributes\Layout;
use Livewire\Component;
/**
* Manage Commerce Matrix entities (M1/M2/M3 hierarchy).
*/
#[Layout('hub::admin.layouts.app')]
class EntityManager extends Component
{
// Modal states
public bool $showModal = false;
public bool $showDeleteModal = false;
public ?int $editingId = null;
public ?int $deletingId = null;
// Form fields
public string $code = '';
public string $name = '';
public string $type = Entity::TYPE_M1_MASTER;
public ?int $parent_id = null;
public ?int $workspace_id = null;
public string $domain = '';
public string $currency = 'GBP';
public string $timezone = 'Europe/London';
public bool $is_active = true;
// View state
public ?int $expandedEntity = null;
/**
* Authorize access - Hades tier only.
*/
public function mount(): void
{
if (! auth()->user()?->isHades()) {
abort(403, 'Hades tier required for entity management.');
}
}
protected function rules(): array
{
$uniqueRule = $this->editingId
? 'unique:commerce_entities,code,'.$this->editingId
: 'unique:commerce_entities,code';
return [
'code' => ['required', 'string', 'max:32', 'alpha_num', $uniqueRule],
'name' => ['required', 'string', 'max:255'],
'type' => ['required', 'in:m1,m2,m3'],
'parent_id' => ['nullable', 'exists:commerce_entities,id'],
'workspace_id' => ['nullable', 'exists:workspaces,id'],
'domain' => ['nullable', 'string', 'max:255'],
'currency' => ['required', 'string', 'size:3'],
'timezone' => ['required', 'string', 'max:50'],
'is_active' => ['boolean'],
];
}
protected array $messages = [
'code.alpha_num' => 'Code must be letters and numbers only.',
'code.unique' => 'This code is already in use.',
];
public function openCreate(?int $parentId = null): void
{
$this->resetForm();
if ($parentId) {
$parent = Entity::find($parentId);
if ($parent) {
$this->parent_id = $parentId;
// Determine child type based on parent
$this->type = match ($parent->type) {
Entity::TYPE_M1_MASTER => Entity::TYPE_M2_FACADE,
Entity::TYPE_M2_FACADE => Entity::TYPE_M3_DROPSHIP,
default => Entity::TYPE_M3_DROPSHIP,
};
// Inherit currency/timezone from parent
$this->currency = $parent->currency;
$this->timezone = $parent->timezone;
}
}
$this->showModal = true;
}
public function openEdit(int $id): void
{
$entity = Entity::findOrFail($id);
$this->editingId = $id;
$this->code = $entity->code;
$this->name = $entity->name;
$this->type = $entity->type;
$this->parent_id = $entity->parent_id;
$this->workspace_id = $entity->workspace_id;
$this->domain = $entity->domain ?? '';
$this->currency = $entity->currency;
$this->timezone = $entity->timezone;
$this->is_active = $entity->is_active;
$this->showModal = true;
}
public function save(): void
{
$this->validate();
// Uppercase code
$code = strtoupper($this->code);
if ($this->editingId) {
$entity = Entity::findOrFail($this->editingId);
// Prevent changing type if has children
if ($entity->type !== $this->type && $entity->children()->exists()) {
session()->flash('error', 'Cannot change type of entity with children.');
return;
}
// Prevent making root entity a child
if (! $entity->parent_id && $this->parent_id) {
session()->flash('error', 'Cannot move root entity under another entity.');
return;
}
// Update path if code changed
$oldCode = $entity->code;
$newPath = $entity->parent
? $entity->parent->path.'/'.$code
: $code;
$entity->update([
'code' => $code,
'name' => $this->name,
'type' => $this->type,
'workspace_id' => $this->workspace_id,
'domain' => $this->domain ?: null,
'currency' => $this->currency,
'timezone' => $this->timezone,
'is_active' => $this->is_active,
'path' => $newPath,
]);
// Update descendant paths if code changed
if ($oldCode !== $code) {
$this->updateDescendantPaths($entity, $oldCode, $code);
}
session()->flash('message', 'Entity updated successfully.');
} else {
// Create new entity
$parent = $this->parent_id ? Entity::find($this->parent_id) : null;
if ($parent) {
$entity = $parent->createChild($code, $this->name, $this->type, [
'workspace_id' => $this->workspace_id,
'domain' => $this->domain ?: null,
'currency' => $this->currency,
'timezone' => $this->timezone,
'is_active' => $this->is_active,
]);
} else {
$entity = Entity::createMaster($code, $this->name, [
'workspace_id' => $this->workspace_id,
'domain' => $this->domain ?: null,
'currency' => $this->currency,
'timezone' => $this->timezone,
'is_active' => $this->is_active,
]);
}
session()->flash('message', 'Entity created successfully.');
}
$this->closeModal();
}
protected function updateDescendantPaths(Entity $entity, string $oldCode, string $newCode): void
{
$oldPathPrefix = str_replace($newCode, $oldCode, $entity->path);
foreach ($entity->getDescendants() as $descendant) {
$descendant->update([
'path' => str_replace($oldPathPrefix, $entity->path, $descendant->path),
]);
}
}
public function confirmDelete(int $id): void
{
$this->deletingId = $id;
$this->showDeleteModal = true;
}
public function delete(): void
{
if (! $this->deletingId) {
return;
}
$entity = Entity::findOrFail($this->deletingId);
// Check for children
if ($entity->children()->exists()) {
session()->flash('error', 'Cannot delete entity with children. Remove children first.');
$this->showDeleteModal = false;
return;
}
// Check for permissions
if ($entity->permissions()->exists()) {
$entity->permissions()->delete();
}
$entity->delete();
session()->flash('message', "Entity '{$entity->name}' deleted successfully.");
$this->showDeleteModal = false;
$this->deletingId = null;
}
public function toggleExpand(int $id): void
{
$this->expandedEntity = $this->expandedEntity === $id ? null : $id;
}
public function toggleActive(int $id): void
{
$entity = Entity::findOrFail($id);
$entity->update(['is_active' => ! $entity->is_active]);
$status = $entity->is_active ? 'activated' : 'deactivated';
session()->flash('message', "Entity '{$entity->name}' {$status}.");
}
public function closeModal(): void
{
$this->showModal = false;
$this->resetForm();
}
protected function resetForm(): void
{
$this->editingId = null;
$this->code = '';
$this->name = '';
$this->type = Entity::TYPE_M1_MASTER;
$this->parent_id = null;
$this->workspace_id = null;
$this->domain = '';
$this->currency = 'GBP';
$this->timezone = 'Europe/London';
$this->is_active = true;
}
public function render()
{
// Get root entities (M1) with nested children
$entities = Entity::whereNull('parent_id')
->with(['children' => function ($query) {
$query->with(['children' => function ($q) {
$q->orderBy('name');
}])->orderBy('name');
}])
->orderBy('name')
->get();
return view('commerce::admin.entity-manager', [
'entities' => $entities,
'workspaces' => Workspace::orderBy('name')->get(['id', 'name']),
'types' => [
Entity::TYPE_M1_MASTER => 'M1 - Master Company',
Entity::TYPE_M2_FACADE => 'M2 - Facade/Storefront',
Entity::TYPE_M3_DROPSHIP => 'M3 - Dropshipper',
],
'currencies' => ['GBP', 'USD', 'EUR'],
'timezones' => [
'Europe/London' => 'London (GMT/BST)',
'Europe/Paris' => 'Paris (CET/CEST)',
'America/New_York' => 'New York (EST/EDT)',
'America/Los_Angeles' => 'Los Angeles (PST/PDT)',
'UTC' => 'UTC',
],
'stats' => [
'total' => Entity::count(),
'm1_count' => Entity::where('type', Entity::TYPE_M1_MASTER)->count(),
'm2_count' => Entity::where('type', Entity::TYPE_M2_FACADE)->count(),
'm3_count' => Entity::where('type', Entity::TYPE_M3_DROPSHIP)->count(),
'active' => Entity::where('is_active', true)->count(),
],
])->layout('hub::admin.layouts.app', ['title' => 'Commerce Entities']);
}
}