From 30528a0c7fef4156ee9e601e5d9cb910b3292b90 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 7 Jun 2026 13:31:22 +0000 Subject: [PATCH] Expand settings beyond QR into a full Events preferences page. Add event notification toggles, new-event defaults that pre-fill creation, and tuck QR styling into a secondary section. Co-authored-by: Cursor --- app/Http/Controllers/Qr/AccountController.php | 18 ++ app/Http/Controllers/Qr/QrCodeController.php | 1 + app/Models/QrSetting.php | 89 ++++++- ..._add_events_prefs_to_qr_settings_table.php | 24 ++ .../partials/type-fields-create.blade.php | 18 +- resources/views/qr/account/settings.blade.php | 241 +++++++++++++----- tests/Feature/QrSettingsTest.php | 22 +- 7 files changed, 340 insertions(+), 73 deletions(-) create mode 100644 database/migrations/2026_06_07_150000_add_events_prefs_to_qr_settings_table.php diff --git a/app/Http/Controllers/Qr/AccountController.php b/app/Http/Controllers/Qr/AccountController.php index 78912c9..333d75c 100644 --- a/app/Http/Controllers/Qr/AccountController.php +++ b/app/Http/Controllers/Qr/AccountController.php @@ -69,6 +69,7 @@ class AccountController extends Controller return view('qr.account.settings', [ 'account' => $account, 'settings' => $settings, + 'eventDefaults' => $settings->resolvedEventDefaults(), 'style' => $style, 'moduleStyles' => QrModuleStyleCatalog::visible(), 'cornerOuterStyles' => QrCornerStyleCatalog::outerStyles(), @@ -85,6 +86,17 @@ class AccountController extends Controller 'notify_email' => ['nullable', 'email', 'max:255'], 'product_updates' => ['nullable', 'boolean'], 'low_balance_alerts' => ['nullable', 'boolean'], + 'notify_registrations' => ['nullable', 'boolean'], + 'notify_payouts' => ['nullable', 'boolean'], + 'event_defaults' => ['nullable', 'array'], + 'event_defaults.currency' => ['nullable', 'in:GHS,USD,NGN,KES'], + 'event_defaults.mode' => ['nullable', 'in:ticketing,contributions,free'], + 'event_defaults.badge_size' => ['nullable', 'in:4x3,4x6,cr80'], + 'event_defaults.brand_color' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'], + 'event_defaults.organizer' => ['nullable', 'string', 'max:120'], + 'event_defaults.registration_open' => ['nullable', 'boolean'], + 'event_defaults.badge_fields' => ['nullable', 'array'], + 'event_defaults.badge_fields.*' => ['nullable', 'string', 'max:40'], 'default_style' => ['nullable', 'array'], 'default_style.foreground' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'], 'default_style.background' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'], @@ -102,12 +114,18 @@ class AccountController extends Controller 'default_style.gradient_rotation' => ['nullable', 'integer', 'min:0', 'max:360'], ]); + $eventDefaults = $data['event_defaults'] ?? []; + $eventDefaults['registration_open'] = $request->boolean('event_defaults.registration_open'); + QrSetting::updateOrCreate( ['user_id' => $account->id], [ 'notify_email' => $data['notify_email'] ?? null, 'product_updates' => $request->boolean('product_updates'), 'low_balance_alerts' => $request->boolean('low_balance_alerts'), + 'notify_registrations' => $request->boolean('notify_registrations'), + 'notify_payouts' => $request->boolean('notify_payouts'), + 'event_defaults' => QrSetting::sanitizeEventDefaults($eventDefaults), 'default_style' => QrSetting::sanitizeDefaultStyle($data['default_style'] ?? null), ], ); diff --git a/app/Http/Controllers/Qr/QrCodeController.php b/app/Http/Controllers/Qr/QrCodeController.php index cc87224..4989ac4 100644 --- a/app/Http/Controllers/Qr/QrCodeController.php +++ b/app/Http/Controllers/Qr/QrCodeController.php @@ -89,6 +89,7 @@ class QrCodeController extends Controller 'ladillWalletBalance' => $this->platformBilling->balanceMinor($account->public_id) / 100, 'topupUrl' => 'https://'.config('app.account_domain').'/wallet', 'accountDefaultStyle' => $qrSettings->resolvedDefaultStyle(), + 'accountEventDefaults' => $qrSettings->resolvedEventDefaults(), ]); } diff --git a/app/Models/QrSetting.php b/app/Models/QrSetting.php index 5719557..8e35010 100644 --- a/app/Models/QrSetting.php +++ b/app/Models/QrSetting.php @@ -7,7 +7,7 @@ use App\Support\Qr\QrTypeCatalog; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -/** Per-account QR Plus preferences (notifications and new-code defaults). */ +/** Per-account Ladill Events preferences (notifications, event defaults, QR style). */ class QrSetting extends Model { protected $fillable = [ @@ -15,16 +15,36 @@ class QrSetting extends Model 'notify_email', 'product_updates', 'low_balance_alerts', + 'notify_registrations', + 'notify_payouts', 'default_type', 'default_style', + 'event_defaults', ]; protected $casts = [ 'product_updates' => 'boolean', 'low_balance_alerts' => 'boolean', + 'notify_registrations' => 'boolean', + 'notify_payouts' => 'boolean', 'default_style' => 'array', + 'event_defaults' => 'array', ]; + /** @return list */ + public static function storableEventDefaultKeys(): array + { + return [ + 'currency', + 'mode', + 'badge_size', + 'brand_color', + 'organizer', + 'registration_open', + 'badge_fields', + ]; + } + /** @return list */ public static function storableStyleKeys(): array { @@ -58,6 +78,73 @@ class QrSetting extends Model return in_array($type, QrTypeCatalog::eventsTypes(), true) ? $type : QrCode::TYPE_EVENT; } + /** @return array */ + public function resolvedEventDefaults(): array + { + $defaults = [ + 'currency' => 'GHS', + 'mode' => 'ticketing', + 'badge_size' => '4x3', + 'brand_color' => '#4f46e5', + 'organizer' => '', + 'registration_open' => true, + 'badge_fields' => ['Company', 'Role'], + ]; + + $stored = collect($this->event_defaults ?? []) + ->only(self::storableEventDefaultKeys()) + ->filter(fn ($value) => $value !== null && $value !== '') + ->all(); + + if (isset($stored['badge_fields']) && is_array($stored['badge_fields'])) { + $stored['badge_fields'] = array_values(array_filter( + array_map('strval', $stored['badge_fields']), + fn (string $field) => trim($field) !== '', + )); + if ($stored['badge_fields'] === []) { + unset($stored['badge_fields']); + } + } + + return array_merge($defaults, $stored); + } + + /** @param array|null $input */ + public static function sanitizeEventDefaults(?array $input): ?array + { + if ($input === null) { + return null; + } + + $allowedModes = ['ticketing', 'contributions', 'free']; + $allowedSizes = ['4x3', '4x6', 'cr80']; + $allowedCurrencies = ['GHS', 'USD', 'NGN', 'KES']; + + $mode = (string) ($input['mode'] ?? 'ticketing'); + $badgeSize = (string) ($input['badge_size'] ?? '4x3'); + $currency = strtoupper(trim((string) ($input['currency'] ?? 'GHS'))); + + $badgeFields = array_values(array_filter( + array_map(fn ($field) => mb_substr(trim((string) $field), 0, 40), (array) ($input['badge_fields'] ?? [])), + fn (string $field) => $field !== '', + )); + + $brandColor = (string) ($input['brand_color'] ?? '#4f46e5'); + if (! preg_match('/^#[0-9a-fA-F]{6}$/', $brandColor)) { + $brandColor = '#4f46e5'; + } + + return [ + 'currency' => in_array($currency, $allowedCurrencies, true) ? $currency : 'GHS', + 'mode' => in_array($mode, $allowedModes, true) ? $mode : 'ticketing', + 'badge_size' => in_array($badgeSize, $allowedSizes, true) ? $badgeSize : '4x3', + 'brand_color' => $brandColor, + 'organizer' => mb_substr(trim((string) ($input['organizer'] ?? '')), 0, 120), + 'registration_open' => filter_var($input['registration_open'] ?? true, FILTER_VALIDATE_BOOL), + 'badge_fields' => $badgeFields !== [] ? $badgeFields : ['Company', 'Role'], + ]; + } + /** @return array */ public function resolvedDefaultStyle(): array { diff --git a/database/migrations/2026_06_07_150000_add_events_prefs_to_qr_settings_table.php b/database/migrations/2026_06_07_150000_add_events_prefs_to_qr_settings_table.php new file mode 100644 index 0000000..7ff6575 --- /dev/null +++ b/database/migrations/2026_06_07_150000_add_events_prefs_to_qr_settings_table.php @@ -0,0 +1,24 @@ +boolean('notify_registrations')->default(true)->after('low_balance_alerts'); + $table->boolean('notify_payouts')->default(true)->after('notify_registrations'); + $table->json('event_defaults')->nullable()->after('default_style'); + }); + } + + public function down(): void + { + Schema::table('qr_settings', function (Blueprint $table) { + $table->dropColumn(['notify_registrations', 'notify_payouts', 'event_defaults']); + }); + } +}; diff --git a/resources/views/qr-codes/partials/type-fields-create.blade.php b/resources/views/qr-codes/partials/type-fields-create.blade.php index 2fee804..5515642 100644 --- a/resources/views/qr-codes/partials/type-fields-create.blade.php +++ b/resources/views/qr-codes/partials/type-fields-create.blade.php @@ -1,3 +1,7 @@ +@php + $ed = $accountEventDefaults ?? []; +@endphp + {{-- URL --}}
@@ -381,10 +385,10 @@ {{-- Event --}}
- @@ -442,7 +446,7 @@
-
@@ -550,13 +554,13 @@
@@ -603,7 +607,7 @@
-
diff --git a/resources/views/qr/account/settings.blade.php b/resources/views/qr/account/settings.blade.php index f3df2f4..2a24f93 100644 --- a/resources/views/qr/account/settings.blade.php +++ b/resources/views/qr/account/settings.blade.php @@ -1,9 +1,14 @@ Settings + @php + $ed = $eventDefaults; + $badgeFieldLabels = old('event_defaults.badge_fields', $ed['badge_fields'] ?? ['Company', 'Role']); + @endphp +

Settings

-

QR Plus notifications and defaults for new codes.

+

Notifications, defaults for new events, and QR preferences.

@if (session('success'))
{{ session('success') }}
@@ -15,7 +20,7 @@

Notifications

-

Where we send QR Plus billing reminders and product updates.

+

Choose what we email you about your events and account.

@@ -27,8 +32,28 @@ + + + +
-

New code defaults

-

Pre-fill the create screen so every new QR starts with your brand look.

+

New event defaults

+

Pre-fill the create flow so every new event starts with your usual setup.

- - -
-
- - -
-
- -
-
- - - @foreach ($moduleStyles as $key => $meta) - + @foreach (['GHS' => 'GHS (Ghanaian Cedi)', 'USD' => 'USD (US Dollar)', 'NGN' => 'NGN (Nigerian Naira)', 'KES' => 'KES (Kenyan Shilling)'] as $code => $label) + @endforeach
- - - @foreach (['L' => 'Low', 'M' => 'Medium', 'Q' => 'Quartile', 'H' => 'High'] as $key => $label) - - @endforeach + + +
- - - @foreach ($cornerOuterStyles as $key => $meta) - + @foreach (['4x3' => '4 × 3 in (landscape)', '4x6' => '4 × 6 in (lanyard)', 'cr80' => 'CR80 card'] as $size => $label) + @endforeach
- - -
-
- -
-
- - -
-
- - Brand color +
- - Default organizer name +
+ +
+ +

Extra attendee fields shown on badges (e.g. Company, Role).

+
+ +
+ +
+ +
+
+ +
+
+

QR code defaults

+

Style applied when you design event QR codes.

+
+ +
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ diff --git a/tests/Feature/QrSettingsTest.php b/tests/Feature/QrSettingsTest.php index 87b4a87..07b870f 100644 --- a/tests/Feature/QrSettingsTest.php +++ b/tests/Feature/QrSettingsTest.php @@ -31,7 +31,9 @@ class QrSettingsTest extends TestCase ->get(route('account.settings')) ->assertOk() ->assertSee('favicon.ico', false) - ->assertSee('New code defaults') + ->assertSee('New event defaults') + ->assertSee('QR code defaults') + ->assertSee('New registrations') ->assertSee('Notifications'); } @@ -44,6 +46,17 @@ class QrSettingsTest extends TestCase 'notify_email' => 'alerts@example.com', 'product_updates' => '1', 'low_balance_alerts' => '0', + 'notify_registrations' => '1', + 'notify_payouts' => '0', + 'event_defaults' => [ + 'currency' => 'USD', + 'mode' => 'free', + 'badge_size' => '4x6', + 'brand_color' => '#aabbcc', + 'organizer' => 'CAPBuSS', + 'registration_open' => '1', + 'badge_fields' => ['Company', 'Title'], + ], 'default_style' => [ 'foreground' => '#112233', 'background' => '#ffffff', @@ -61,6 +74,11 @@ class QrSettingsTest extends TestCase $this->assertSame('alerts@example.com', $settings->notify_email); $this->assertTrue($settings->product_updates); $this->assertFalse($settings->low_balance_alerts); + $this->assertTrue($settings->notify_registrations); + $this->assertFalse($settings->notify_payouts); + $this->assertSame('USD', $settings->resolvedEventDefaults()['currency']); + $this->assertSame('free', $settings->resolvedEventDefaults()['mode']); + $this->assertSame('CAPBuSS', $settings->resolvedEventDefaults()['organizer']); $this->assertSame('#112233', $settings->resolvedDefaultStyle()['foreground']); $this->assertSame('scan_me', $settings->resolvedDefaultStyle()['frame_style']); } @@ -72,6 +90,7 @@ class QrSettingsTest extends TestCase 'user_id' => $user->id, 'default_type' => QrCode::TYPE_EVENT, 'default_style' => ['foreground' => '#aabbcc', 'module_style' => 'dots'], + 'event_defaults' => ['mode' => 'contributions', 'organizer' => 'Test Org', 'brand_color' => '#ff00aa'], ]); Http::fake([ @@ -83,6 +102,7 @@ class QrSettingsTest extends TestCase ->assertOk() ->assertSee('favicon.ico', false) ->assertSee('#aabbcc', false) + ->assertSee('Test Org', false) ->assertSee('"event"', false); } }