Skip to content

Commit d1c6f36

Browse files
authored
Merge pull request #19 from techenby/amn/item-qr
Add QR Code Generation
2 parents f3533d1 + 43e1dcb commit d1c6f36

File tree

14 files changed

+657
-14
lines changed

14 files changed

+657
-14
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Actions\Inventory;
6+
7+
use App\Models\Item;
8+
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
9+
use BaconQrCode\Renderer\ImageRenderer;
10+
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
11+
use BaconQrCode\Writer;
12+
13+
class GenerateItemQrCode
14+
{
15+
public function handle(Item $item): array
16+
{
17+
$renderer = new ImageRenderer(
18+
new RendererStyle(300),
19+
new SvgImageBackEnd,
20+
);
21+
22+
if ($item->children()->exists()) {
23+
$url = route('inventory.index', ['parentId' => $item->id]);
24+
} else {
25+
$url = route('inventory.show', ['item' => $item]);
26+
}
27+
28+
return [
29+
'svg' => new Writer($renderer)->writeString($url),
30+
'name' => $item->name,
31+
'url' => $url,
32+
];
33+
}
34+
}

app/Policies/ItemPolicy.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Policies;
6+
7+
use App\Models\Item;
8+
use App\Models\User;
9+
10+
class ItemPolicy
11+
{
12+
public function viewAny(User $user): bool
13+
{
14+
return true;
15+
}
16+
17+
public function view(User $user, Item $item): bool
18+
{
19+
return $item->team_id === $user->current_team_id;
20+
}
21+
22+
public function create(User $user): bool
23+
{
24+
return true;
25+
}
26+
27+
public function update(User $user, Item $item): bool
28+
{
29+
return $item->team_id === $user->current_team_id;
30+
}
31+
32+
public function delete(User $user, Item $item): bool
33+
{
34+
return $item->team_id === $user->current_team_id;
35+
}
36+
37+
public function restore(User $user, Item $item): bool
38+
{
39+
return $item->team_id === $user->current_team_id;
40+
}
41+
42+
public function forceDelete(User $user, Item $item): bool
43+
{
44+
return $item->team_id === $user->current_team_id;
45+
}
46+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"license": "MIT",
1111
"require": {
1212
"php": "^8.2",
13+
"bacon/bacon-qr-code": "^3.0",
1314
"dedoc/scramble": "^0.13.14",
1415
"laravel/fortify": "^1.30",
1516
"laravel/framework": "^12.0",

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/views/pages/inventory/modals/item-form.blade.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@teleport('body')
12
<flux:modal name="item-form" class="md:w-96">
23
<form wire:submit="save" class="space-y-6">
34
<flux:heading size="lg">{{ $form->editingItem ? __('Edit Item') : __('Add Item') }}</flux:heading>
@@ -67,3 +68,4 @@
6768
</div>
6869
</form>
6970
</flux:modal>
71+
@endteleport
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@teleport('body')
2+
<flux:modal name="qr-code" class="md:w-96">
3+
<div class="space-y-6">
4+
<div class="space-y-1">
5+
<flux:heading size="lg">{{ __('QR Code') }}</flux:heading>
6+
7+
<flux:text>{{ $qrCode['name'] ?? '' }}</flux:text>
8+
<flux:link :href="$qrCode['url'] ?? ''">{{ $qrCode['url'] ?? '' }}</flux:link>
9+
</div>
10+
11+
<div class="flex justify-center">
12+
{!! $qrCode['svg'] ?? '' !!}
13+
</div>
14+
</div>
15+
</flux:modal>
16+
@endteleport

resources/views/pages/inventory/⚡index/index.blade.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
@forelse ($this->items as $item)
5656
<flux:table.row :key="$item->id">
5757
<flux:table.cell>
58-
<flux:link wire:click="navigateDown({{ $item->id }})" inset="top bottom" class="!flex !items-center gap-3">
58+
<flux:link wire:click="navigateDown({{ $item->id }})" as="button" inset="top bottom" class="!flex !items-center gap-3">
5959
<flux:avatar size="xs" :icon="$item->type->getIcon()" :color="$item->type->getIconColor()" icon:variant="outline" />
6060
<span>{{ $item->truncated_name }}</span>
6161
</flux:link>
@@ -70,6 +70,7 @@
7070

7171
<flux:menu>
7272
<flux:menu.item wire:click="edit({{ $item->id }})" icon="pencil">{{ __('Edit') }}</flux:menu.item>
73+
<flux:menu.item wire:click="showQrCode({{ $item->id }})" icon="qr-code">{{ __('QR Code') }}</flux:menu.item>
7374
<flux:menu.item wire:click="delete({{ $item->id }})" variant="danger" icon="trash" wire:confirm="{{ __('Are you sure you want to delete this item?') }}">{{ __('Delete') }}</flux:menu.item>
7475
</flux:menu>
7576
</flux:dropdown>
@@ -85,7 +86,6 @@
8586
</flux:table.rows>
8687
</flux:table>
8788

88-
@teleport('body')
8989
@include('pages.inventory.modals.item-form')
90-
@endteleport
90+
@include('pages.inventory.modals.qr-code')
9191
</section>

resources/views/pages/inventory/⚡index/index.php

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<?php
22

3+
use App\Actions\Inventory\GenerateItemQrCode;
34
use App\Livewire\Forms\Inventory\ImportItemsForm;
45
use App\Livewire\Forms\Inventory\ItemForm;
56
use App\Livewire\Traits\WithSearching;
67
use App\Livewire\Traits\WithSorting;
8+
use App\Models\Item;
79
use Illuminate\Database\Eloquent\Collection;
810
use Illuminate\Pagination\LengthAwarePaginator;
911
use Illuminate\Support\Collection as BaseCollection;
@@ -25,6 +27,8 @@
2527
public ItemForm $form;
2628
public ImportItemsForm $importForm;
2729

30+
public ?array $qrCode = null;
31+
2832
#[Url]
2933
public ?int $parentId = null;
3034

@@ -79,19 +83,22 @@ public function create(): void
7983

8084
public function delete(int $id): void
8185
{
82-
Auth::user()->currentTeam->items()
83-
->where('id', $id)
84-
->firstOrFail()
85-
->delete();
86+
$item = Auth::user()->currentTeam->items()->findOrFail($id);
87+
88+
$this->authorize('delete', $item);
89+
90+
$item->delete();
8691

8792
unset($this->items, $this->parentItems);
8893
}
8994

9095
public function edit(int $id): void
9196
{
92-
$this->form->load(
93-
Auth::user()->currentTeam->items()->findOrFail($id)
94-
);
97+
$item = Auth::user()->currentTeam->items()->findOrFail($id);
98+
99+
$this->authorize('update', $item);
100+
101+
$this->form->load($item);
95102
$this->modal('item-form')->show();
96103
}
97104

@@ -105,6 +112,13 @@ public function import(): void
105112

106113
public function navigateDown(int $id): void
107114
{
115+
$item = $this->items->firstWhere('id', $id);
116+
if ($item !== null && $item->children_count === 0) {
117+
$this->redirectRoute('inventory.show', ['item' => $item]);
118+
119+
return;
120+
}
121+
108122
$this->parentId = $id;
109123
unset($this->items, $this->parentItems, $this->breadcrumbs);
110124
}
@@ -118,13 +132,32 @@ public function navigateUp(): void
118132
}
119133
}
120134

135+
public function showQrCode(int $id): void
136+
{
137+
$item = Auth::user()->currentTeam->items()->findOrFail($id);
138+
139+
$this->authorize('view', $item);
140+
141+
$this->qrCode = resolve(GenerateItemQrCode::class)->handle($item);
142+
143+
$this->modal('qr-code')->show();
144+
}
145+
121146
public function removeMetadata(int $index): void
122147
{
148+
if ($this->form->editingItem) {
149+
$this->authorize('update', $this->form->editingItem);
150+
}
151+
123152
$this->form->removeMetadata($index);
124153
}
125154

126155
public function removePhoto(): void
127156
{
157+
if ($this->form->editingItem) {
158+
$this->authorize('update', $this->form->editingItem);
159+
}
160+
128161
if ($this->form->photo) {
129162
$this->form->photo->delete();
130163
$this->form->photo = null;
@@ -136,6 +169,12 @@ public function removePhoto(): void
136169

137170
public function save(): void
138171
{
172+
if ($this->form->editingItem) {
173+
$this->authorize('update', $this->form->editingItem);
174+
} else {
175+
$this->authorize('create', Item::class);
176+
}
177+
139178
$this->form->save();
140179
$this->modal('item-form')->close();
141180
unset($this->items, $this->parentItems);

resources/views/pages/inventory/⚡index/index.test.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
$bedroom = Item::factory()->for($user->currentTeam)->location()->create(['name' => 'Bedroom']);
9595
$closet = Item::factory()->for($user->currentTeam)->bin()->childOf($bedroom)->create(['name' => 'Right Closet']);
9696
$tote = Item::factory()->for($user->currentTeam)->bin()->childOf($closet)->create(['name' => 'Game Tote']);
97+
$game = Item::factory()->for($user->currentTeam)->bin()->childOf($tote)->create(['name' => 'Catan']);
9798

9899
Livewire::actingAs($user)
99100
->test('pages::inventory.index')
@@ -128,12 +129,25 @@
128129
->assertSeeHtml('<span>Right Closet</span>')
129130
->assertDontSeeHtml('<span>Game Tote</span>');
130131
});
132+
133+
test('clicking item without children redirects to show', function () {
134+
$user = User::factory()->withTeam()->create();
135+
$parent = Item::factory()->for($user->currentTeam)->location()->create(['name' => 'Bedroom']);
136+
$child = Item::factory()->for($user->currentTeam)->childOf($parent)->bin()->create(['name' => 'Closet']);
137+
138+
Livewire::actingAs($user)
139+
->test('pages::inventory.index')
140+
->call('navigateDown', $parent->id)
141+
->call('navigateDown', $child->id)
142+
->assertRedirect(route('inventory.show', ['item' => $child]));
143+
});
131144
});
132145

133146
describe('can create and edit', function () {
134147
test('create pre-fills parent_id with current parentId', function () {
135148
$user = User::factory()->withTeam()->create();
136149
$parent = Item::factory()->for($user->currentTeam)->location()->create(['name' => 'Bedroom']);
150+
Item::factory()->for($user->currentTeam)->childOf($parent)->bin()->create(['name' => 'Tote']);
137151

138152
Livewire::actingAs($user)
139153
->test('pages::inventory.index')
@@ -498,7 +512,7 @@
498512

499513
Livewire::actingAs($user)
500514
->test('pages::inventory.index')
501-
->call('navigateDown', $parent->id)
515+
->set('parentId', $parent->id)
502516
->set('importForm.file', amazonFixtureUpload())
503517
->call('import')
504518
->assertHasNoErrors();
@@ -536,3 +550,36 @@
536550
->assertHasErrors('importForm.file');
537551
});
538552
});
553+
554+
describe('can generate qr codes', function () {
555+
test('can show qr code for an item', function () {
556+
$user = User::factory()->withTeam()->create();
557+
$item = Item::factory()->for($user->currentTeam)->create(['name' => 'Guitar']);
558+
559+
Livewire::actingAs($user)
560+
->test('pages::inventory.index')
561+
->call('showQrCode', $item->id)
562+
->assertSet('qrCode.name', 'Guitar')
563+
->assertNotSet('qrCode.svg', '');
564+
});
565+
566+
test('qr code svg contains valid svg markup', function () {
567+
$user = User::factory()->withTeam()->create();
568+
$item = Item::factory()->for($user->currentTeam)->create();
569+
570+
$component = Livewire::actingAs($user)
571+
->test('pages::inventory.index')
572+
->call('showQrCode', $item->id);
573+
574+
expect($component->get('qrCode.svg'))->toContain('<svg');
575+
});
576+
577+
test('cannot show qr code for an item from another team', function () {
578+
$user = User::factory()->withTeam()->create();
579+
$otherItem = Item::factory()->create();
580+
581+
Livewire::actingAs($user)
582+
->test('pages::inventory.index')
583+
->call('showQrCode', $otherItem->id);
584+
})->throws(ModelNotFoundException::class);
585+
});

0 commit comments

Comments
 (0)