php-commerce/View/Modal/Admin/EntityManager.php

317 lines
9.8 KiB
PHP
Raw Normal View History

2026-01-27 00:24:22 +00:00
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Admin;
2026-01-27 00:24:22 +00:00
use Core\Mod\Commerce\Models\Entity;
use Core\Tenant\Models\Workspace;
2026-01-27 00:24:22 +00:00
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']);
}
}