feat: phase 6 — dashboard widgets, sales & stock reports, user management, activity log
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Phase6;
|
||||
|
||||
use App\Filament\Pages\Dashboard;
|
||||
use App\Filament\Pages\Reports\StockReport;
|
||||
use App\Filament\Pages\Settings\ActivityLogs;
|
||||
use App\Filament\Widgets\CriticalStock;
|
||||
use App\Filament\Widgets\DashboardStats;
|
||||
use App\Models\User;
|
||||
use App\Services\OrderService;
|
||||
use App\Services\UserAdminService;
|
||||
use Livewire\Livewire;
|
||||
use Tests\Support\InsForgeFake;
|
||||
use Tests\TestCase;
|
||||
|
||||
class Phase6Test extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
InsForgeFake::reset();
|
||||
InsForgeFake::fakeRest();
|
||||
|
||||
InsForgeFake::seed('products', [
|
||||
'id' => 'p-1',
|
||||
'code' => 'BERAS-001',
|
||||
'name' => 'Beras Premium',
|
||||
'cost_price' => 13000,
|
||||
'stock_minimum' => 50,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function actingAsRole(string $role): User
|
||||
{
|
||||
$user = new User;
|
||||
|
||||
$user->forceFill([
|
||||
'id' => "eeee9999-0000-0000-0000-{$role}00001",
|
||||
'name' => ucfirst($role),
|
||||
'email' => "{$role}@beraspro.test",
|
||||
'role' => $role,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function test_dashboard_renders_for_operator(): void
|
||||
{
|
||||
$this->actingAsRole('operator');
|
||||
|
||||
InsForgeFake::seed('stocks', [
|
||||
'id' => 's1',
|
||||
'product_id' => 'p-1',
|
||||
'quantity' => 5,
|
||||
'products' => ['code' => 'BERAS-001', 'name' => 'Beras Premium', 'stock_minimum' => 50, 'cost_price' => 13000],
|
||||
]);
|
||||
|
||||
Livewire::test(Dashboard::class)->assertSuccessful();
|
||||
$this->assertCount(3, (new Dashboard)->getWidgets());
|
||||
|
||||
Livewire::test(DashboardStats::class)
|
||||
->assertSuccessful()
|
||||
->assertSee('Pending')
|
||||
->assertSee('Komplain Terbuka');
|
||||
|
||||
Livewire::test(CriticalStock::class)
|
||||
->assertSuccessful()
|
||||
->assertSee('Beras Premium')
|
||||
->assertSee('kritis');
|
||||
}
|
||||
|
||||
public function test_sales_report_aggregates_completed_orders(): void
|
||||
{
|
||||
$this->actingAsRole('admin');
|
||||
|
||||
InsForgeFake::seed('orders', [
|
||||
'id' => 'o1',
|
||||
'order_status' => OrderService::STATUS_COMPLETED,
|
||||
'deleted_at' => null,
|
||||
'order_items' => [
|
||||
['quantity' => 3, 'subtotal' => 45000, 'products' => ['code' => 'BERAS-001', 'name' => 'Beras Premium']],
|
||||
['quantity' => 2, 'subtotal' => 30000, 'products' => ['code' => 'BERAS-001', 'name' => 'Beras Premium']],
|
||||
],
|
||||
]);
|
||||
|
||||
$rows = app(OrderService::class)->salesPerProduct('2026-08-01', '2026-08-31');
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame(5, $rows[0]['qty']);
|
||||
$this->assertSame(75000, $rows[0]['revenue']);
|
||||
}
|
||||
|
||||
public function test_sales_report_page_denied_for_operator(): void
|
||||
{
|
||||
$this->actingAsRole('operator');
|
||||
|
||||
$this->get('/admin/reports/sales')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_stock_report_renders_for_admin(): void
|
||||
{
|
||||
$this->actingAsRole('admin');
|
||||
|
||||
InsForgeFake::seed('stocks', [
|
||||
'id' => 's2',
|
||||
'product_id' => 'p-1',
|
||||
'quantity' => 20,
|
||||
'products' => ['code' => 'BERAS-001', 'name' => 'Beras Premium', 'stock_minimum' => 50, 'cost_price' => 13000],
|
||||
]);
|
||||
|
||||
Livewire::test(StockReport::class)
|
||||
->assertSuccessful()
|
||||
->assertSee('Beras Premium')
|
||||
->assertSee('kritis');
|
||||
}
|
||||
|
||||
public function test_users_page_denied_for_operator(): void
|
||||
{
|
||||
$this->actingAsRole('operator');
|
||||
|
||||
$this->get('/admin/settings/users')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_create_user_via_admin_api_creates_verified_mirror(): void
|
||||
{
|
||||
$this->actingAsRole('admin');
|
||||
|
||||
$result = app(UserAdminService::class)->createUser([
|
||||
'name' => 'Operator Baru',
|
||||
'email' => 'baru@beraspro.test',
|
||||
'password' => 'Rahasia#123',
|
||||
'role' => 'operator',
|
||||
], auth()->user());
|
||||
|
||||
$this->assertArrayHasKey('id', $result);
|
||||
|
||||
// Mirror row dibuat dengan role benar.
|
||||
$mirror = array_values(array_filter(
|
||||
InsForgeFake::$store['users'] ?? [],
|
||||
fn (array $row): bool => $row['email'] === 'baru@beraspro.test',
|
||||
));
|
||||
$this->assertCount(1, $mirror);
|
||||
$this->assertSame('operator', reset($mirror)['role']);
|
||||
|
||||
// email terverifikasi via rawSql UPDATE.
|
||||
$verified = array_values(array_filter(InsForgeFake::$rawSqlQueries, fn (string $q): bool => str_contains($q, 'email_verified = true')));
|
||||
$this->assertNotEmpty($verified);
|
||||
|
||||
// Activity log tertulis.
|
||||
$log = end(InsForgeFake::$store['activity_logs']);
|
||||
$this->assertSame('user_created', $log['action']);
|
||||
}
|
||||
|
||||
public function test_activity_logs_page_renders_for_admin(): void
|
||||
{
|
||||
$this->actingAsRole('admin');
|
||||
|
||||
InsForgeFake::seed('activity_logs', [
|
||||
'id' => 'l1',
|
||||
'action' => 'order_created',
|
||||
'entity_type' => 'orders',
|
||||
'description' => 'Order ORD-X dibuat.',
|
||||
]);
|
||||
|
||||
Livewire::test(ActivityLogs::class)
|
||||
->assertSuccessful()
|
||||
->assertSee('order_created');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Phase6;
|
||||
|
||||
use App\Filament\Widgets\CriticalStock;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Tests\Support\InsForgeFake;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WidProbe extends TestCase
|
||||
{
|
||||
public function test_probe(): void
|
||||
{
|
||||
InsForgeFake::reset();
|
||||
InsForgeFake::fakeRest();
|
||||
|
||||
InsForgeFake::seed('products', [
|
||||
'id' => 'p-1', 'code' => 'BERAS-001', 'name' => 'Beras Premium',
|
||||
'cost_price' => 13000, 'stock_minimum' => 50, 'is_active' => true,
|
||||
]);
|
||||
|
||||
$user = new User;
|
||||
$user->forceFill(['id' => 'u1', 'role' => 'operator', 'is_active' => true]);
|
||||
$this->actingAs($user);
|
||||
|
||||
InsForgeFake::seed('stocks', [
|
||||
'id' => 's1',
|
||||
'product_id' => 'p-1',
|
||||
'quantity' => 5,
|
||||
'products' => ['code' => 'BERAS-001', 'name' => 'Beras Premium', 'stock_minimum' => 50, 'cost_price' => 13000],
|
||||
]);
|
||||
|
||||
$html = Livewire::test(CriticalStock::class)->html();
|
||||
echo 'len=', strlen($html), "\n";
|
||||
echo 'Beras: ', str_contains($html, 'Beras Premium') ? 'Y' : 'N', "\n";
|
||||
echo 'kritis: ', str_contains($html, 'kritis') ? 'Y' : 'N', "\n";
|
||||
echo 'no-data: ', str_contains($html, 'tidak ada data') || str_contains($html, 'empty') ? 'Y' : 'N', "\n";
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,20 @@ class InsForgeFake
|
||||
/** @var list<array{function: string, args: array<string, mixed>}> */
|
||||
public static array $rpcCalls = [];
|
||||
|
||||
/** @var array<string, string> email => auth uuid */
|
||||
public static array $authUsersByEmail = [];
|
||||
|
||||
/** @var list<string> */
|
||||
public static array $rawSqlQueries = [];
|
||||
|
||||
public static string $accessToken = '';
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$store = [];
|
||||
self::$rpcCalls = [];
|
||||
self::$authUsersByEmail = [];
|
||||
self::$rawSqlQueries = [];
|
||||
}
|
||||
|
||||
public static function seed(string $table, array $row): void
|
||||
@@ -71,6 +79,30 @@ class InsForgeFake
|
||||
{
|
||||
Http::fake([
|
||||
'*/api/auth/refresh' => Http::response(['accessToken' => self::$accessToken], 200),
|
||||
'*/api/auth/users' => function (Request $request) {
|
||||
if (strtoupper($request->method()) === 'POST') {
|
||||
$payload = $request->data();
|
||||
self::$authUsersByEmail[$payload['email'] ?? ''] = 'auth-'.md5((string) ($payload['email'] ?? ''));
|
||||
|
||||
return Http::response(['accessToken' => null, 'requireEmailVerification' => true], 200);
|
||||
}
|
||||
|
||||
return Http::response([], 200);
|
||||
},
|
||||
'*/api/database/advance/rawsql*' => function (Request $request) {
|
||||
$query = (string) ($request->data()['query'] ?? '');
|
||||
|
||||
self::$rawSqlQueries[] = $query;
|
||||
|
||||
if (str_contains($query, 'FROM auth.users') && str_contains(strtoupper($query), 'SELECT')) {
|
||||
preg_match("/email = '([^']+)'/", $query, $m);
|
||||
$id = self::$authUsersByEmail[$m[1] ?? ''] ?? null;
|
||||
|
||||
return Http::response(['rows' => $id ? [['id' => $id]] : []], 200);
|
||||
}
|
||||
|
||||
return Http::response(['rows' => []], 200);
|
||||
},
|
||||
'*/api/database/rpc/*' => function (Request $request) {
|
||||
$function = str_replace('/api/database/rpc/', '', (string) parse_url($request->url(), PHP_URL_PATH));
|
||||
$args = $request->data();
|
||||
|
||||
Reference in New Issue
Block a user