feat/041-inventory-ui #44
30
app/Filament/Pages/InventoryCoverage.php
Normal file
30
app/Filament/Pages/InventoryCoverage.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use UnitEnum;
|
||||
|
||||
class InventoryCoverage extends Page
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-table-cells';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||
|
||||
protected static ?string $navigationLabel = 'Coverage';
|
||||
|
||||
protected string $view = 'filament.pages.inventory-coverage';
|
||||
|
||||
/**
|
||||
* @var array<int, array<string, mixed>>
|
||||
*/
|
||||
public array $supportedTypes = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$types = config('tenantpilot.supported_policy_types', []);
|
||||
|
||||
$this->supportedTypes = is_array($types) ? $types : [];
|
||||
}
|
||||
}
|
||||
36
app/Filament/Pages/InventoryLanding.php
Normal file
36
app/Filament/Pages/InventoryLanding.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Resources\InventoryItemResource;
|
||||
use App\Filament\Resources\InventorySyncRunResource;
|
||||
use App\Models\Tenant;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use UnitEnum;
|
||||
|
||||
class InventoryLanding extends Page
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-squares-2x2';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||
|
||||
protected static ?string $navigationLabel = 'Inventory';
|
||||
|
||||
protected string $view = 'filament.pages.inventory-landing';
|
||||
|
||||
public function getInventoryItemsUrl(): string
|
||||
{
|
||||
return InventoryItemResource::getUrl('index', tenant: Tenant::current());
|
||||
}
|
||||
|
||||
public function getSyncRunsUrl(): string
|
||||
{
|
||||
return InventorySyncRunResource::getUrl('index', tenant: Tenant::current());
|
||||
}
|
||||
|
||||
public function getCoverageUrl(): string
|
||||
{
|
||||
return InventoryCoverage::getUrl(tenant: Tenant::current());
|
||||
}
|
||||
}
|
||||
161
app/Filament/Resources/InventoryItemResource.php
Normal file
161
app/Filament/Resources/InventoryItemResource.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\InventoryItemResource\Pages;
|
||||
use App\Models\InventoryItem;
|
||||
use App\Models\Tenant;
|
||||
use BackedEnum;
|
||||
use Filament\Actions;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use UnitEnum;
|
||||
|
||||
class InventoryItemResource extends Resource
|
||||
{
|
||||
protected static ?string $model = InventoryItem::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema;
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Section::make('Inventory Item')
|
||||
->schema([
|
||||
TextEntry::make('display_name')->label('Name'),
|
||||
TextEntry::make('policy_type')
|
||||
->label('Type')
|
||||
->badge()
|
||||
->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['label'] ?? (string) $record->policy_type),
|
||||
TextEntry::make('category')
|
||||
->badge()
|
||||
->state(fn (InventoryItem $record): string => $record->category
|
||||
?: (static::typeMeta($record->policy_type)['category'] ?? 'Unknown')),
|
||||
TextEntry::make('platform')->badge(),
|
||||
TextEntry::make('external_id')->label('External ID'),
|
||||
TextEntry::make('last_seen_at')->label('Last seen')->dateTime(),
|
||||
TextEntry::make('last_seen_run_id')
|
||||
->label('Last sync run')
|
||||
->url(function (InventoryItem $record): ?string {
|
||||
if (! $record->last_seen_run_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return InventorySyncRunResource::getUrl('view', ['record' => $record->last_seen_run_id], tenant: Tenant::current());
|
||||
})
|
||||
->openUrlInNewTab(),
|
||||
TextEntry::make('support_restore')
|
||||
->label('Restore')
|
||||
->badge()
|
||||
->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['restore'] ?? 'enabled'),
|
||||
TextEntry::make('support_risk')
|
||||
->label('Risk')
|
||||
->badge()
|
||||
->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['risk'] ?? 'normal'),
|
||||
])
|
||||
->columns(2)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Metadata (Safe Subset)')
|
||||
->schema([
|
||||
ViewEntry::make('meta_jsonb')
|
||||
->label('')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(fn (InventoryItem $record) => $record->meta_jsonb ?? [])
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
$typeOptions = collect(config('tenantpilot.supported_policy_types', []))
|
||||
->mapWithKeys(fn (array $row) => [($row['type'] ?? null) => ($row['label'] ?? $row['type'] ?? null)])
|
||||
->filter(fn ($label, $type) => is_string($type) && $type !== '' && is_string($label) && $label !== '')
|
||||
->all();
|
||||
|
||||
$categoryOptions = collect(config('tenantpilot.supported_policy_types', []))
|
||||
->mapWithKeys(fn (array $row) => [($row['category'] ?? null) => ($row['category'] ?? null)])
|
||||
->filter(fn ($value, $key) => is_string($key) && $key !== '')
|
||||
->all();
|
||||
|
||||
return $table
|
||||
->defaultSort('last_seen_at', 'desc')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('display_name')
|
||||
->label('Name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('policy_type')
|
||||
->label('Type')
|
||||
->badge()
|
||||
->formatStateUsing(fn (?string $state): string => static::typeMeta($state)['label'] ?? (string) $state),
|
||||
Tables\Columns\TextColumn::make('category')
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('platform')
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('last_seen_at')
|
||||
->label('Last seen')
|
||||
->since(),
|
||||
Tables\Columns\TextColumn::make('lastSeenRun.status')
|
||||
->label('Run')
|
||||
->badge(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('policy_type')
|
||||
->options($typeOptions)
|
||||
->searchable(),
|
||||
Tables\Filters\SelectFilter::make('category')
|
||||
->options($categoryOptions)
|
||||
->searchable(),
|
||||
])
|
||||
->actions([
|
||||
Actions\ViewAction::make(),
|
||||
])
|
||||
->bulkActions([]);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$tenantId = Tenant::current()->getKey();
|
||||
|
||||
return parent::getEloquentQuery()
|
||||
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
||||
->with('lastSeenRun');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListInventoryItems::route('/'),
|
||||
'view' => Pages\ViewInventoryItem::route('/{record}'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label:?string,category:?string,restore:?string,risk:?string}|array<string,mixed>
|
||||
*/
|
||||
private static function typeMeta(?string $type): array
|
||||
{
|
||||
if ($type === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect(config('tenantpilot.supported_policy_types', []))
|
||||
->firstWhere('type', $type) ?? [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\InventoryItemResource\Pages;
|
||||
|
||||
use App\Filament\Resources\InventoryItemResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListInventoryItems extends ListRecords
|
||||
{
|
||||
protected static string $resource = InventoryItemResource::class;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\InventoryItemResource\Pages;
|
||||
|
||||
use App\Filament\Resources\InventoryItemResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewInventoryItem extends ViewRecord
|
||||
{
|
||||
protected static string $resource = InventoryItemResource::class;
|
||||
}
|
||||
137
app/Filament/Resources/InventorySyncRunResource.php
Normal file
137
app/Filament/Resources/InventorySyncRunResource.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\InventorySyncRunResource\Pages;
|
||||
use App\Models\InventorySyncRun;
|
||||
use App\Models\Tenant;
|
||||
use BackedEnum;
|
||||
use Filament\Actions;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use UnitEnum;
|
||||
|
||||
class InventorySyncRunResource extends Resource
|
||||
{
|
||||
protected static ?string $model = InventorySyncRun::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clock';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema;
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Section::make('Sync Run')
|
||||
->schema([
|
||||
TextEntry::make('status')
|
||||
->badge()
|
||||
->color(fn (InventorySyncRun $record): string => static::statusColor($record->status)),
|
||||
TextEntry::make('selection_hash')->label('Selection hash')->copyable(),
|
||||
TextEntry::make('started_at')->dateTime(),
|
||||
TextEntry::make('finished_at')->dateTime(),
|
||||
TextEntry::make('items_observed_count')->label('Observed')->numeric(),
|
||||
TextEntry::make('items_upserted_count')->label('Upserted')->numeric(),
|
||||
TextEntry::make('errors_count')->label('Errors')->numeric(),
|
||||
TextEntry::make('had_errors')->label('Had errors')->badge(),
|
||||
])
|
||||
->columns(2)
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Selection Payload')
|
||||
->schema([
|
||||
ViewEntry::make('selection_payload')
|
||||
->label('')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(fn (InventorySyncRun $record) => $record->selection_payload ?? [])
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
Section::make('Error Summary')
|
||||
->schema([
|
||||
ViewEntry::make('error_codes')
|
||||
->label('Error codes')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(fn (InventorySyncRun $record) => $record->error_codes ?? [])
|
||||
->columnSpanFull(),
|
||||
ViewEntry::make('error_context')
|
||||
->label('Safe error context')
|
||||
->view('filament.infolists.entries.snapshot-json')
|
||||
->state(fn (InventorySyncRun $record) => $record->error_context ?? [])
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->defaultSort('id', 'desc')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn (InventorySyncRun $record): string => static::statusColor($record->status)),
|
||||
Tables\Columns\TextColumn::make('selection_hash')
|
||||
->label('Selection')
|
||||
->copyable()
|
||||
->limit(12),
|
||||
Tables\Columns\TextColumn::make('started_at')->since(),
|
||||
Tables\Columns\TextColumn::make('finished_at')->since(),
|
||||
Tables\Columns\TextColumn::make('items_observed_count')
|
||||
->label('Observed')
|
||||
->numeric(),
|
||||
Tables\Columns\TextColumn::make('items_upserted_count')
|
||||
->label('Upserted')
|
||||
->numeric(),
|
||||
Tables\Columns\TextColumn::make('errors_count')
|
||||
->label('Errors')
|
||||
->numeric(),
|
||||
])
|
||||
->actions([
|
||||
Actions\ViewAction::make(),
|
||||
])
|
||||
->bulkActions([]);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$tenantId = Tenant::current()->getKey();
|
||||
|
||||
return parent::getEloquentQuery()
|
||||
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId));
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListInventorySyncRuns::route('/'),
|
||||
'view' => Pages\ViewInventorySyncRun::route('/{record}'),
|
||||
];
|
||||
}
|
||||
|
||||
private static function statusColor(?string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
InventorySyncRun::STATUS_SUCCESS => 'success',
|
||||
InventorySyncRun::STATUS_PARTIAL => 'warning',
|
||||
InventorySyncRun::STATUS_FAILED => 'danger',
|
||||
InventorySyncRun::STATUS_SKIPPED => 'gray',
|
||||
InventorySyncRun::STATUS_RUNNING => 'info',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\InventorySyncRunResource\Pages;
|
||||
|
||||
use App\Filament\Resources\InventorySyncRunResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListInventorySyncRuns extends ListRecords
|
||||
{
|
||||
protected static string $resource = InventorySyncRunResource::class;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\InventorySyncRunResource\Pages;
|
||||
|
||||
use App\Filament\Resources\InventorySyncRunResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewInventorySyncRun extends ViewRecord
|
||||
{
|
||||
protected static string $resource = InventorySyncRunResource::class;
|
||||
}
|
||||
28
resources/views/filament/pages/inventory-coverage.blade.php
Normal file
28
resources/views/filament/pages/inventory-coverage.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
<x-filament::page>
|
||||
<x-filament::section>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200 dark:border-gray-800">
|
||||
<th class="py-2 pr-4 font-medium">Type</th>
|
||||
<th class="py-2 pr-4 font-medium">Label</th>
|
||||
<th class="py-2 pr-4 font-medium">Category</th>
|
||||
<th class="py-2 pr-4 font-medium">Restore</th>
|
||||
<th class="py-2 pr-4 font-medium">Risk</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($supportedTypes as $row)
|
||||
<tr>
|
||||
<td class="py-2 pr-4">{{ $row['type'] ?? '' }}</td>
|
||||
<td class="py-2 pr-4">{{ $row['label'] ?? '' }}</td>
|
||||
<td class="py-2 pr-4">{{ $row['category'] ?? '' }}</td>
|
||||
<td class="py-2 pr-4">{{ $row['restore'] ?? 'enabled' }}</td>
|
||||
<td class="py-2 pr-4">{{ $row['risk'] ?? 'normal' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament::page>
|
||||
23
resources/views/filament/pages/inventory-landing.blade.php
Normal file
23
resources/views/filament/pages/inventory-landing.blade.php
Normal file
@ -0,0 +1,23 @@
|
||||
<x-filament::page>
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Browse inventory items, inspect sync runs, and review coverage/capabilities.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<x-filament::button tag="a" :href="$this->getInventoryItemsUrl()">
|
||||
Inventory Items
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button tag="a" color="gray" :href="$this->getSyncRunsUrl()">
|
||||
Sync Runs
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button tag="a" color="gray" :href="$this->getCoverageUrl()">
|
||||
Coverage
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament::page>
|
||||
@ -1,9 +1,9 @@
|
||||
# Tasks: Inventory UI
|
||||
|
||||
- [ ] T001 Inventory landing page + navigation
|
||||
- [ ] T002 Inventory list views per type/category
|
||||
- [ ] T003 Inventory detail view with capability/support indicator
|
||||
- [ ] T004 Sync runs list + detail
|
||||
- [ ] T005 Coverage/capabilities view derived from config/contracts
|
||||
- [ ] T006 Authorization + tenant isolation tests
|
||||
- [ ] T007 Performance sanity checks (pagination/search)
|
||||
- [x] T001 Inventory landing page + navigation
|
||||
- [x] T002 Inventory list views per type/category
|
||||
- [x] T003 Inventory detail view with capability/support indicator
|
||||
- [x] T004 Sync runs list + detail
|
||||
- [x] T005 Coverage/capabilities view derived from config/contracts
|
||||
- [x] T006 Authorization + tenant isolation tests
|
||||
- [x] T007 Performance sanity checks (pagination/search)
|
||||
|
||||
41
tests/Feature/Filament/InventoryItemResourceTest.php
Normal file
41
tests/Feature/Filament/InventoryItemResourceTest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\InventoryItemResource;
|
||||
use App\Models\InventoryItem;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
|
||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
test('inventory items are listed for the active tenant', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$otherTenant = Tenant::factory()->create();
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'display_name' => 'Item A',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'external_id' => 'item-a',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
'tenant_id' => $otherTenant->getKey(),
|
||||
'display_name' => 'Item B',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'external_id' => 'item-b',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
$otherTenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(InventoryItemResource::getUrl('index', tenant: $tenant))
|
||||
->assertOk()
|
||||
->assertSee('Item A')
|
||||
->assertDontSee('Item B');
|
||||
});
|
||||
26
tests/Feature/Filament/InventoryPagesTest.php
Normal file
26
tests/Feature/Filament/InventoryPagesTest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Pages\InventoryCoverage;
|
||||
use App\Filament\Pages\InventoryLanding;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
|
||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
test('inventory landing and coverage pages load for a tenant', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(InventoryLanding::getUrl(tenant: $tenant))
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(InventoryCoverage::getUrl(tenant: $tenant))
|
||||
->assertOk()
|
||||
->assertSee('Coverage');
|
||||
});
|
||||
37
tests/Feature/Filament/InventorySyncRunResourceTest.php
Normal file
37
tests/Feature/Filament/InventorySyncRunResourceTest.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\InventorySyncRunResource;
|
||||
use App\Models\InventorySyncRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
|
||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
test('inventory sync runs are listed for the active tenant', function () {
|
||||
$tenant = Tenant::factory()->create();
|
||||
$otherTenant = Tenant::factory()->create();
|
||||
|
||||
InventorySyncRun::factory()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'selection_hash' => str_repeat('a', 64),
|
||||
'status' => InventorySyncRun::STATUS_SUCCESS,
|
||||
]);
|
||||
|
||||
InventorySyncRun::factory()->create([
|
||||
'tenant_id' => $otherTenant->getKey(),
|
||||
'selection_hash' => str_repeat('b', 64),
|
||||
'status' => InventorySyncRun::STATUS_SUCCESS,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenant->getKey() => ['role' => 'owner'],
|
||||
$otherTenant->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(InventorySyncRunResource::getUrl('index', tenant: $tenant))
|
||||
->assertOk()
|
||||
->assertSee(str_repeat('a', 12))
|
||||
->assertDontSee(str_repeat('b', 12));
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user