php-commerce/View/Modal/Admin/OrderManager.php
Snider 8f27fe85c3 refactor: update Tenant module imports after namespace migration
Updates all references from Core\Mod\Tenant to Core\Tenant following
the monorepo separation. The Tenant module now lives in its own package
with the simplified namespace.

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

376 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Admin;
use Core\Mod\Commerce\Models\Order;
use Core\Tenant\Models\Workspace;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithPagination;
#[Title('Orders')]
class OrderManager extends Component
{
use WithPagination;
// Bulk selection
public array $selected = [];
public bool $selectAll = false;
// Filters
public string $search = '';
public string $statusFilter = '';
public string $typeFilter = '';
public string $dateRange = '';
public ?int $workspaceFilter = null;
// Order detail modal
public bool $showDetailModal = false;
public ?Order $selectedOrder = null;
// Status update modal
public bool $showStatusModal = false;
public string $newStatus = '';
public string $statusNote = '';
/**
* Authorize access - Hades tier only.
*/
public function mount(): void
{
if (! auth()->user()?->isHades()) {
abort(403, 'Hades tier required for order management.');
}
}
public function updatingSearch(): void
{
$this->resetPage();
$this->selected = [];
$this->selectAll = false;
}
public function updatingStatusFilter(): void
{
$this->resetPage();
$this->selected = [];
$this->selectAll = false;
}
public function updatingDateRange(): void
{
$this->resetPage();
$this->selected = [];
$this->selectAll = false;
}
public function updatedSelectAll(bool $value): void
{
if ($value) {
$this->selected = $this->orders->pluck('id')->map(fn ($id) => (string) $id)->all();
} else {
$this->selected = [];
}
}
public function exportSelected(): void
{
if (empty($this->selected)) {
session()->flash('error', __('commerce::commerce.bulk.no_selection'));
return;
}
$orders = Order::whereIn('id', $this->selected)->get();
$csv = "Order Number,Customer,Email,Type,Status,Total,Currency,Created\n";
foreach ($orders as $order) {
$csv .= sprintf(
"%s,%s,%s,%s,%s,%s,%s,%s\n",
$order->order_number,
str_replace(',', ' ', $order->billing_name ?: $order->user?->name),
$order->billing_email ?: $order->user?->email,
$order->type ?? 'unknown',
$order->status,
number_format($order->total, 2),
$order->currency,
$order->created_at->format('Y-m-d H:i:s')
);
}
$this->dispatch('download-csv', filename: 'orders-export-'.now()->format('Y-m-d').'.csv', content: $csv);
session()->flash('message', __('commerce::commerce.bulk.export_success', ['count' => count($this->selected)]));
$this->selected = [];
$this->selectAll = false;
}
public function bulkUpdateStatus(string $status): void
{
if (empty($this->selected)) {
session()->flash('error', __('commerce::commerce.bulk.no_selection'));
return;
}
$validStatuses = ['pending', 'processing', 'paid', 'failed', 'refunded', 'cancelled'];
if (! in_array($status, $validStatuses)) {
session()->flash('error', 'Invalid status selected.');
return;
}
$count = Order::whereIn('id', $this->selected)->update([
'status' => $status,
]);
session()->flash('message', __('commerce::commerce.bulk.status_updated', ['count' => $count, 'status' => ucfirst($status)]));
$this->selected = [];
$this->selectAll = false;
}
public function viewOrder(int $id): void
{
$this->selectedOrder = Order::with([
'workspace',
'user',
'items',
'coupon',
'invoice.payment',
])->findOrFail($id);
$this->showDetailModal = true;
}
public function openStatusChange(int $id): void
{
$this->selectedOrder = Order::findOrFail($id);
$this->newStatus = $this->selectedOrder->status;
$this->statusNote = '';
$this->showStatusModal = true;
}
public function updateStatus(): void
{
if (! $this->selectedOrder) {
return;
}
$validStatuses = ['pending', 'processing', 'paid', 'failed', 'refunded', 'cancelled'];
if (! in_array($this->newStatus, $validStatuses)) {
session()->flash('error', 'Invalid status selected.');
return;
}
$oldStatus = $this->selectedOrder->status;
$this->selectedOrder->update([
'status' => $this->newStatus,
'metadata' => array_merge($this->selectedOrder->metadata ?? [], [
'status_history' => array_merge(
$this->selectedOrder->metadata['status_history'] ?? [],
[[
'from' => $oldStatus,
'to' => $this->newStatus,
'note' => $this->statusNote ?: null,
'by' => auth()->id(),
'at' => now()->toIso8601String(),
]]
),
]),
]);
if ($this->newStatus === 'paid' && ! $this->selectedOrder->paid_at) {
$this->selectedOrder->update(['paid_at' => now()]);
}
session()->flash('message', "Order status updated from {$oldStatus} to {$this->newStatus}.");
$this->closeStatusModal();
}
public function closeDetailModal(): void
{
$this->showDetailModal = false;
$this->selectedOrder = null;
}
public function closeStatusModal(): void
{
$this->showStatusModal = false;
$this->selectedOrder = null;
$this->newStatus = '';
$this->statusNote = '';
}
#[Computed]
public function orders()
{
return Order::query()
->with(['workspace', 'user'])
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('order_number', 'like', "%{$this->search}%")
->orWhere('billing_email', 'like', "%{$this->search}%")
->orWhere('billing_name', 'like', "%{$this->search}%");
});
})
->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter))
->when($this->typeFilter, fn ($q) => $q->where('type', $this->typeFilter))
->when($this->dateRange, function ($query) {
$startDate = match ($this->dateRange) {
'today' => now()->startOfDay(),
'7d' => now()->subDays(7)->startOfDay(),
'30d' => now()->subDays(30)->startOfDay(),
'90d' => now()->subDays(90)->startOfDay(),
'this_month' => now()->startOfMonth(),
'last_month' => now()->subMonth()->startOfMonth(),
default => null,
};
if ($startDate) {
$query->where('created_at', '>=', $startDate);
}
if ($this->dateRange === 'last_month') {
$query->where('created_at', '<', now()->startOfMonth());
}
})
->when($this->workspaceFilter, fn ($q) => $q->where('workspace_id', $this->workspaceFilter))
->latest()
->paginate(25);
}
#[Computed]
public function workspaces()
{
return Workspace::orderBy('name')->get(['id', 'name']);
}
#[Computed]
public function statuses(): array
{
return [
'pending' => 'Pending',
'processing' => 'Processing',
'paid' => 'Paid',
'failed' => 'Failed',
'refunded' => 'Refunded',
'cancelled' => 'Cancelled',
];
}
#[Computed]
public function types(): array
{
return [
'new_subscription' => 'New Subscription',
'renewal' => 'Renewal',
'upgrade' => 'Upgrade',
'downgrade' => 'Downgrade',
'addon' => 'Add-on',
'one_time' => 'One-time',
];
}
#[Computed]
public function dateRangeOptions(): array
{
return [
'today' => __('commerce::commerce.orders.date_range.today'),
'7d' => __('commerce::commerce.orders.date_range.7d'),
'30d' => __('commerce::commerce.orders.date_range.30d'),
'90d' => __('commerce::commerce.orders.date_range.90d'),
'this_month' => __('commerce::commerce.orders.date_range.this_month'),
'last_month' => __('commerce::commerce.orders.date_range.last_month'),
];
}
#[Computed]
public function tableColumns(): array
{
return [
'Order',
'Customer',
'Type',
['label' => 'Total', 'align' => 'right'],
['label' => 'Status', 'align' => 'center'],
'Date',
['label' => 'Actions', 'align' => 'center'],
];
}
#[Computed]
public function tableRowIds(): array
{
return $this->orders->pluck('id')->all();
}
#[Computed]
public function tableRows(): array
{
$statusColors = [
'pending' => 'amber',
'processing' => 'blue',
'paid' => 'green',
'failed' => 'red',
'refunded' => 'purple',
'cancelled' => 'gray',
];
return $this->orders->map(function ($o) use ($statusColors) {
$totalLines = [['bold' => $o->currency.' '.number_format($o->total, 2)]];
if ($o->discount_amount > 0) {
$totalLines[] = ['muted' => '-'.number_format($o->discount_amount, 2).' discount'];
}
return [
[
'lines' => [
['mono' => $o->order_number],
['muted' => $o->workspace?->name],
],
],
[
'lines' => [
['bold' => $o->billing_name ?: $o->user?->name],
['muted' => $o->billing_email ?: $o->user?->email],
],
],
['badge' => str_replace('_', ' ', ucfirst($o->type ?? 'unknown')), 'color' => 'gray'],
['lines' => $totalLines],
['badge' => ucfirst($o->status), 'color' => $statusColors[$o->status] ?? 'gray'],
[
'lines' => [
['bold' => $o->created_at->format('d M Y')],
['muted' => $o->created_at->format('H:i')],
],
],
[
'actions' => [
['icon' => 'eye', 'click' => "viewOrder({$o->id})", 'title' => 'View details'],
['icon' => 'pencil', 'click' => "openStatusChange({$o->id})", 'title' => 'Change status'],
],
],
];
})->all();
}
public function render()
{
return view('commerce::admin.order-manager')
->layout('hub::admin.layouts.app', ['title' => 'Orders']);
}
}