php-commerce/View/Modal/Admin/OrderManager.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

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\Mod\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']);
}
}