Felder leer lassen = Geschlossen. Der Status "Geöffnet/Geschlossen" wird automatisch aus diesen Zeiten berechnet.
+ Felder leer lassen = Geschlossen. Der Status
+ "Geöffnet/Geschlossen" wird automatisch aus diesen Zeiten berechnet.
- @foreach([
- ['label' => 'Montag', 'open' => 'hoursMondayOpen', 'close' => 'hoursMondayClose'],
- ['label' => 'Dienstag', 'open' => 'hoursTuesdayOpen', 'close' => 'hoursTuesdayClose'],
- ['label' => 'Mittwoch', 'open' => 'hoursWednesdayOpen', 'close' => 'hoursWednesdayClose'],
- ['label' => 'Donnerstag', 'open' => 'hoursThursdayOpen', 'close' => 'hoursThursdayClose'],
- ['label' => 'Freitag', 'open' => 'hoursFridayOpen', 'close' => 'hoursFridayClose'],
- ['label' => 'Samstag', 'open' => 'hoursSaturdayOpen', 'close' => 'hoursSaturdayClose'],
- ['label' => 'Sonntag', 'open' => 'hoursSundayOpen', 'close' => 'hoursSundayClose'],
- ] as $row)
+ @foreach ([['label' => 'Montag', 'open' => 'hoursMondayOpen', 'close' => 'hoursMondayClose'], ['label' => 'Dienstag', 'open' => 'hoursTuesdayOpen', 'close' => 'hoursTuesdayClose'], ['label' => 'Mittwoch', 'open' => 'hoursWednesdayOpen', 'close' => 'hoursWednesdayClose'], ['label' => 'Donnerstag', 'open' => 'hoursThursdayOpen', 'close' => 'hoursThursdayClose'], ['label' => 'Freitag', 'open' => 'hoursFridayOpen', 'close' => 'hoursFridayClose'], ['label' => 'Samstag', 'open' => 'hoursSaturdayOpen', 'close' => 'hoursSaturdayClose'], ['label' => 'Sonntag', 'open' => 'hoursSundayOpen', 'close' => 'hoursSundayClose']] as $row)
@php
$isClosedDay = empty($this->{$row['open']}) && empty($this->{$row['close']});
@endphp
-
-
{{ $row['label'] }}
+
+
{{ $row['label'] }}
-
+
-
+
-
+
{{ $isClosedDay ? 'Geschlossen' : 'Geöffnet' }}
@@ -111,7 +127,7 @@
{{ __('Sonderöffnung heute') }}
- @if($overrideOpenToday || $overrideCloseToday)
+ @if ($overrideOpenToday || $overrideCloseToday)
{{ __('Zurücksetzen') }}
@@ -126,7 +142,7 @@
@error('overrideCloseToday') {{ $message }} @enderror
- --}}
+ --}}
{{-- Nächster Termin
@@ -139,7 +155,7 @@
@error('nextAppointmentTime') {{ $message }} @enderror
- --}}
+ --}}
{{-- Kontakt --}}
@@ -147,10 +163,15 @@
- @error('contactPhone') {{ $message }} @enderror
+ @error('contactPhone')
+ {{ $message }}
+ @enderror
-
- @error('contactEmail') {{ $message }} @enderror
+
+ @error('contactEmail')
+ {{ $message }}
+ @enderror
diff --git a/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php b/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php
index ceda105a..1c61b448 100644
--- a/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php
+++ b/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php
@@ -27,7 +27,7 @@
:key="'picker-b2in-header-logo-' . $context . '-' . $version->id" />
-
+
diff --git a/tests/Feature/CabinetQuickStatusTest.php b/tests/Feature/CabinetQuickStatusTest.php
index acd77995..63c73819 100644
--- a/tests/Feature/CabinetQuickStatusTest.php
+++ b/tests/Feature/CabinetQuickStatusTest.php
@@ -16,6 +16,12 @@ test('page is accessible without login', function () {
->assertSeeLivewire(QuickStatus::class);
});
+test('page accepts key query parameter used in admin link', function () {
+ $this->get('/info/status?key='.VALID_KEY)
+ ->assertSuccessful()
+ ->assertSeeLivewire(QuickStatus::class);
+});
+
test('page shows forbidden when key is missing', function () {
Livewire::test(QuickStatus::class, ['k' => ''])
->assertSet('authorized', false)
diff --git a/tests/Feature/DisplayVersionApiTest.php b/tests/Feature/DisplayVersionApiTest.php
index f6af4e05..51688c75 100644
--- a/tests/Feature/DisplayVersionApiTest.php
+++ b/tests/Feature/DisplayVersionApiTest.php
@@ -176,6 +176,7 @@ test('returns playlist with offers config', function () {
'bullets' => [],
'disclaimer' => '',
'qr_url' => 'https://cabinet-bielefeld.de',
+ 'qr_image_url' => 'https://portal.b2in.test/storage/display-qr-codes/cabinet.png',
'qr_title' => 'Reservieren',
'contact' => "0521 98620100\nTel. oder WhatsApp",
'show_brand_text' => false,
@@ -193,6 +194,7 @@ test('returns playlist with offers config', function () {
$response->assertJsonPath('playlist.0.slides.0.title', 'GOYA Sideboard');
$response->assertJsonPath('playlist.0.slides.0.price', '489 €');
$response->assertJsonPath('playlist.0.slides.0.qr_url', 'https://cabinet-bielefeld.de');
+ $response->assertJsonPath('playlist.0.slides.0.qr_image_url', 'https://portal.b2in.test/storage/display-qr-codes/cabinet.png');
});
test('returns playlist with multiple versions in order', function () {
@@ -449,7 +451,25 @@ test('display player keeps previews in a strict 9 by 16 viewport', function () {
->toContain('this.settings.footer_claim')
->toContain('this.settings.footer_url')
->toContain('this.settings.header_logo_url')
+ ->toContain('/_cabinet/assets/b2in-logo-positive.svg')
+ ->toContain("url.startsWith('../assets/')")
+ ->toContain("!url.includes('/') && url.endsWith('.svg')")
->toContain('this.settings.qr_label')
+ ->toContain('function setQrImage')
+ ->toContain('function qrCodeImageUrl')
+ ->toContain('options.imageUrl')
+ ->toContain('content.qr_image_url')
+ ->toContain('this.settings.qr_image_url')
+ ->toContain('slide.qr_image_url')
+ ->toContain("img.loading = 'eager'")
+ ->toContain('maxAttempts')
+ ->toContain('cb=${Date.now()}-${attempt}')
+ ->toContain('offer-slide--no-header')
+ ->toContain('offer-slide--no-bottom')
+ ->toContain('offer-bottom--info-only')
+ ->toContain('offer-bottom--qr-only')
+ ->toContain('offer-info--compact')
+ ->toContain('const showBottom = showInfo || showQrBox')
->toContain('this.settings.show_footer !== false')
->toContain('this.settings.show_logo !== false')
->toContain('this.settings.show_claim !== false')
diff --git a/tests/Feature/DisplayVersionTest.php b/tests/Feature/DisplayVersionTest.php
index de0bbc2a..5a8ab568 100644
--- a/tests/Feature/DisplayVersionTest.php
+++ b/tests/Feature/DisplayVersionTest.php
@@ -10,6 +10,8 @@ use App\Models\DisplayPlaylistItem;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use App\Models\User;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Livewire\Livewire;
@@ -70,6 +72,7 @@ test('can create a display version', function () {
expect($version->settings)->toBeArray();
expect($version->settings)->toHaveKey('theme');
expect($version->settings)->toHaveKey('header_logo_url');
+ expect($version->settings['header_logo_url'])->toBe('/_cabinet/assets/b2in-logo-positive.svg');
expect($version->settings)->toHaveKey('footer_url');
});
@@ -446,6 +449,56 @@ test('can add a slide item to offers version', function () {
expect($item->content['qr_url'])->toBe('https://cabinet-bielefeld.de');
});
+test('slide editor generates and stores a qr image when a qr url is saved', function () {
+ Storage::fake('public');
+ Http::fake([
+ 'api.qrserver.com/*' => Http::response('fake-png', 200, ['Content-Type' => 'image/png']),
+ ]);
+
+ $user = User::factory()->create();
+ $version = DisplayVersion::factory()->create(['type' => 'offers']);
+
+ Livewire::actingAs($user)
+ ->test(DisplayVersionEditor::class, ['displayVersion' => $version])
+ ->call('openItemModal', null, 'slide')
+ ->set('slideTitle', 'QR Slide')
+ ->set('slideQrUrl', 'cabinet-bielefeld.de')
+ ->call('saveItem');
+
+ $item = DisplayVersionItem::where('display_version_id', $version->id)->first();
+
+ expect($item->content['qr_url'])->toBe('cabinet-bielefeld.de')
+ ->and($item->content['qr_image_path'])->toStartWith('display-qr-codes/')
+ ->and($item->content['qr_image_url'])->toContain('/storage/display-qr-codes/');
+
+ expect(Storage::disk('public')->exists($item->content['qr_image_path']))->toBeTrue();
+ Http::assertSentCount(1);
+});
+
+test('b2in settings generate and store a qr image when a qr url is saved', function () {
+ Storage::fake('public');
+ Http::fake([
+ 'api.qrserver.com/*' => Http::response('fake-png', 200, ['Content-Type' => 'image/png']),
+ ]);
+
+ $user = User::factory()->create();
+ $version = DisplayVersion::factory()->create(['type' => 'b2in']);
+
+ Livewire::actingAs($user)
+ ->test(DisplayVersionEditor::class, ['displayVersion' => $version])
+ ->call('openSettingsModal')
+ ->set('settings.qr_url', 'b2in.eu')
+ ->call('saveSettings');
+
+ $settings = $version->fresh()->settings;
+
+ expect($settings['qr_image_path'])->toStartWith('display-qr-codes/')
+ ->and($settings['qr_image_url'])->toContain('/storage/display-qr-codes/');
+
+ expect(Storage::disk('public')->exists($settings['qr_image_path']))->toBeTrue();
+ Http::assertSentCount(1);
+});
+
test('can add a slide with bullets to offers version', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['type' => 'offers']);
diff --git a/tests/Feature/FluxAssetRouteTest.php b/tests/Feature/FluxAssetRouteTest.php
new file mode 100644
index 00000000..2d2a4781
--- /dev/null
+++ b/tests/Feature/FluxAssetRouteTest.php
@@ -0,0 +1,31 @@
+get('https://'.config('domains.domain_portal').'/flux/flux.js?id=test')
+ ->assertSuccessful()
+ ->assertHeader('content-type', 'text/javascript; charset=utf-8');
+});
+
+test('portal domain serves minified flux javascript route in production mode', function () {
+ config(['app.debug' => false]);
+
+ $this->get('https://'.config('domains.domain_portal').'/flux/flux.min.js?id=test')
+ ->assertSuccessful()
+ ->assertHeader('content-type', 'text/javascript; charset=utf-8');
+});
+
+test('flux pro vendor bundle contains alpine components used by the admin ui', function () {
+ $path = base_path('vendor/livewire/flux-pro/dist/flux.js');
+
+ expect(is_file($path))->toBeTrue('Flux Pro must be installed via composer (auth.json on live).');
+
+ expect(file_get_contents($path))->toContain('fluxModal');
+});
diff --git a/tests/Feature/PublishFluxAssetsTest.php b/tests/Feature/PublishFluxAssetsTest.php
new file mode 100644
index 00000000..bdd54057
--- /dev/null
+++ b/tests/Feature/PublishFluxAssetsTest.php
@@ -0,0 +1,30 @@
+artisan('flux:publish-assets')
+ ->assertSuccessful();
+
+ expect(is_file(public_path('flux/flux.js')))->toBeTrue();
+ expect(is_file(public_path('flux/flux.min.js')))->toBeTrue();
+ expect(filesize(public_path('flux/flux.min.js')))->toBeGreaterThan(1000);
+
+ File::deleteDirectory($targetDir);
+});
+
+test('composer install and update publish flux assets', function () {
+ $composer = json_decode((string) file_get_contents(base_path('composer.json')), true, flags: JSON_THROW_ON_ERROR);
+ $expectedCommand = '@php artisan flux:publish-assets --ansi --force';
+
+ expect($composer['scripts']['post-install-cmd'] ?? [])->toContain($expectedCommand);
+ expect($composer['scripts']['post-update-cmd'] ?? [])->toContain($expectedCommand);
+});