feat: phase 6 — dashboard widgets, sales & stock reports, user management, activity log

This commit is contained in:
sean
2026-08-25 19:25:40 +07:00
parent 28937a786e
commit feba1ec839
18 changed files with 1011 additions and 5 deletions
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Filament\Widgets;
use App\Services\StockService;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
class CriticalStock extends TableWidget
{
protected static ?int $sort = 1;
protected int|string|array $columnSpan = 'full';
public function table(Table $table): Table
{
$service = app(StockService::class);
$rows = collect($service->paginate(perPage: 100)->items())
->filter(fn (array $row): bool => (int) $row['quantity'] <= (int) data_get($row, 'products.stock_minimum', 0))
->values();
return $table
->heading('Stok Kritis')
->records(fn () => $rows)
->columns([
TextColumn::make('products.code')->label('Kode')->badge(),
TextColumn::make('products.name')->label('Produk'),
TextColumn::make('quantity')->label('Saldo')->numeric()->color('danger'),
TextColumn::make('products.stock_minimum')->label('Min.'),
TextColumn::make('status')
->badge()
->color('danger')
->state(fn (array $record): string => (int) $record['quantity'] <= (int) data_get($record, 'products.stock_minimum', 0) ? 'kritis' : 'aman'),
])
->paginated(false);
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Filament\Widgets;
use App\Services\ComplaintService;
use App\Services\InsForge\InsForgeClient;
use App\Services\OrderService;
use App\Services\StockService;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class DashboardStats extends StatsOverviewWidget
{
protected static ?int $sort = 0;
protected ?string $heading = 'Ringkasan Operasional';
public function getStats(): array
{
$orderService = app(OrderService::class);
$counts = $orderService->countByStatus();
$openComplaints = app(InsForgeClient::class)->countRecords('complaints', [
'status' => ComplaintService::STATUS_OPEN,
]);
$critical = collect(app(StockService::class)->paginate(perPage: 100)->items())
->filter(fn (array $row): bool => (int) $row['quantity'] <= (int) data_get($row, 'products.stock_minimum', 0))
->count();
return [
Stat::make('Pending', (string) ($counts[OrderService::STATUS_PENDING] ?? 0))->color('gray'),
Stat::make('Diproses', (string) ($counts[OrderService::STATUS_PROCESSING] ?? 0))->color('info'),
Stat::make('Dikirim', (string) ($counts[OrderService::STATUS_DELIVERING] ?? 0))->color('warning'),
Stat::make('Selesai', (string) ($counts[OrderService::STATUS_COMPLETED] ?? 0))->color('success'),
Stat::make('Komplain Terbuka', (string) $openComplaints)->color($openComplaints > 0 ? 'danger' : 'success'),
Stat::make('Stok Kritis', (string) $critical)->color($critical > 0 ? 'danger' : 'success'),
];
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Widgets;
use App\Services\OrderService;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
use Illuminate\Support\Collection;
class RecentOrders extends TableWidget
{
protected static ?int $sort = 2;
protected int|string|array $columnSpan = 'full';
public function table(Table $table): Table
{
$service = app(OrderService::class);
return $table
->heading('Pesanan Terbaru')
->records(fn () => new Collection($service->recent(8)))
->columns([
TextColumn::make('order_number')->label('No. Order')->badge(),
TextColumn::make('customers.name')->label('Pelanggan'),
TextColumn::make('order_date')->label('Tgl')->date('d M Y'),
TextColumn::make('order_status')
->label('Status')
->badge()
->color(fn (string $state): string => match ($state) {
'pending' => 'gray',
'processing' => 'info',
'delivering' => 'warning',
'completed' => 'success',
default => 'danger',
})
->formatStateUsing(fn (string $state): string => match ($state) {
'pending' => 'Pending',
'processing' => 'Diproses',
'delivering' => 'Dikirim',
'completed' => 'Selesai',
default => 'Batal',
}),
])
->paginated(false);
}
}