Add custom company logo upload and simplify kiosk branding.
Deploy Ladill Frontdesk / deploy (push) Successful in 23s

Show Ladill Frontdesk or uploaded logo top-left on kiosk; allow logo upload in onboarding and organization settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-27 21:49:17 +00:00
co-authored by Cursor
parent 0e28b59b10
commit dfda39df6e
9 changed files with 172 additions and 27 deletions
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Frontdesk;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
use App\Services\Frontdesk\OrganizationResolver;
use App\Support\OrganizationBranding;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -43,9 +44,16 @@ class OnboardingController extends Controller
'visitor_policy' => ['nullable', 'string', 'max:5000'],
'badge_expiry_hours' => ['nullable', 'integer', 'min:1', 'max:24'],
'kiosk_reset_seconds' => ['nullable', 'integer', 'min:30', 'max:600'],
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
]);
$this->organizations->completeOnboarding($request->user(), $validated);
$organization = $this->organizations->completeOnboarding($request->user(), $validated);
if ($request->hasFile('logo')) {
$organization->update([
'logo_path' => OrganizationBranding::storeLogo($organization, $request->file('logo')),
]);
}
return redirect()->route('frontdesk.dashboard')->with('success', 'Welcome to Ladill Frontdesk!');
}
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
use App\Models\Branch;
use App\Services\Frontdesk\FrontdeskPermissions;
use App\Support\OrganizationBranding;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -51,6 +52,8 @@ class SettingsController extends Controller
'notification_channels.*' => ['string', 'in:'.implode(',', array_keys(config('frontdesk.notification_channels')))],
'notification_events' => ['nullable', 'array'],
'report_daily_recipients' => ['nullable', 'string', 'max:2000'],
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
'remove_logo' => ['nullable', 'boolean'],
]);
$settings = $organization->settings ?? [];
@@ -81,9 +84,21 @@ class SettingsController extends Controller
);
}
$logoPath = $organization->logo_path;
if ($request->boolean('remove_logo')) {
OrganizationBranding::deleteStoredLogo($organization);
$logoPath = null;
}
if ($request->hasFile('logo')) {
$logoPath = OrganizationBranding::storeLogo($organization, $request->file('logo'));
}
$organization->update([
'name' => $validated['name'],
'timezone' => $validated['timezone'],
'logo_path' => $logoPath,
'settings' => $settings,
]);
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Support;
use App\Models\Organization;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class OrganizationBranding
{
public const DEFAULT_LOGO = 'images/logo/ladillfrontdesk-logo.svg';
public static function logoUrl(Organization $organization): string
{
if ($organization->logo_path && Storage::disk('public')->exists($organization->logo_path)) {
$version = $organization->updated_at?->getTimestamp() ?? time();
return Storage::disk('public')->url($organization->logo_path).'?v='.$version;
}
$path = public_path(self::DEFAULT_LOGO);
return asset(self::DEFAULT_LOGO).'?v='.(@filemtime($path) ?: '1');
}
public static function logoAlt(Organization $organization): string
{
return $organization->logo_path ? $organization->name : 'Ladill Frontdesk';
}
public static function hasCustomLogo(Organization $organization): bool
{
return $organization->logo_path !== null
&& Storage::disk('public')->exists($organization->logo_path);
}
public static function storeLogo(Organization $organization, UploadedFile $file): string
{
self::deleteStoredLogo($organization);
return $file->store('frontdesk/organizations/'.$organization->id, 'public');
}
public static function deleteStoredLogo(Organization $organization): void
{
if ($organization->logo_path) {
Storage::disk('public')->delete($organization->logo_path);
}
}
}
@@ -1,7 +1,3 @@
@php
$fdIcon = 'images/launcher-icons/frontdesk.svg';
$fdLogo = 'images/logo/ladillfrontdesk-logo.svg';
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
@@ -25,19 +21,13 @@
<div class="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-200/40 blur-3xl"></div>
</div>
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col">
<div class="fixed inset-x-0 top-0 z-10 h-1.5 bg-gradient-to-r from-indigo-600 to-violet-600"></div>
@include('frontdesk.partials.kiosk-brand')
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col pt-20">
<template x-if="step === 'welcome'">
<div class="flex min-h-screen flex-col items-center justify-center px-6 py-12 text-center">
<div class="fixed inset-x-0 top-0 h-1.5 bg-gradient-to-r from-indigo-600 to-violet-600"></div>
<img src="{{ asset($fdIcon) }}?v={{ @filemtime(public_path($fdIcon)) ?: '1' }}"
alt=""
class="h-24 w-24 drop-shadow-sm">
<img src="{{ asset($fdLogo) }}?v={{ @filemtime(public_path($fdLogo)) ?: '1' }}"
alt="Ladill Frontdesk"
class="mt-5 h-7 w-auto">
<div class="mt-14 max-w-xl">
<div class="flex min-h-[calc(100vh-5rem)] flex-col items-center justify-center px-6 py-12 text-center">
<div class="max-w-xl">
<p class="text-sm font-semibold uppercase tracking-wider text-indigo-600">Visitor check-in</p>
<h1 class="mt-3 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
Welcome to {{ $organization->name }}
@@ -53,14 +43,7 @@
</template>
<template x-if="step !== 'welcome'">
<div class="flex min-h-screen flex-col px-6 py-6">
<header class="mb-6 text-center">
<img src="{{ asset($fdIcon) }}?v={{ @filemtime(public_path($fdIcon)) ?: '1' }}"
alt=""
class="mx-auto h-10 w-10">
<p class="mt-3 text-sm font-medium text-slate-500">{{ $organization->name }}</p>
</header>
<div class="flex min-h-[calc(100vh-5rem)] flex-col px-6 py-6">
<main class="flex-1">
<template x-if="step === 'type'">
<div>
@@ -3,7 +3,7 @@
<h1 class="text-2xl font-semibold text-slate-900">Welcome to Ladill Frontdesk</h1>
<p class="mt-2 text-sm text-slate-600">Set up your organization to start managing visitors.</p>
<form method="POST" action="{{ route('frontdesk.onboarding.store') }}" class="mt-8 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
<form method="POST" action="{{ route('frontdesk.onboarding.store') }}" enctype="multipart/form-data" class="mt-8 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
<div>
@@ -24,6 +24,13 @@
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Company logo (optional)</label>
<p class="mt-1 text-xs text-slate-500">Replaces the Ladill Frontdesk logo on your visitor kiosk.</p>
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/svg+xml"
class="mt-2 block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-indigo-700">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Timezone</label>
<select name="timezone" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@@ -0,0 +1,8 @@
@php
use App\Support\OrganizationBranding;
@endphp
<div class="fixed left-0 top-0 z-20 p-6">
<img src="{{ OrganizationBranding::logoUrl($organization) }}"
alt="{{ OrganizationBranding::logoAlt($organization) }}"
class="h-8 w-auto max-w-[220px] object-contain object-left">
</div>
@@ -6,7 +6,7 @@
<h1 class="text-xl font-semibold text-slate-900">Settings</h1>
<p class="text-sm text-slate-500">{{ $organization->name }}</p>
<form method="POST" action="{{ route('frontdesk.settings.update') }}" class="mt-6 max-w-2xl space-y-6">
<form method="POST" action="{{ route('frontdesk.settings.update') }}" enctype="multipart/form-data" class="mt-6 max-w-2xl space-y-6">
@csrf @method('PUT')
<section class="rounded-2xl border border-slate-200 bg-white p-5 space-y-4">
@@ -24,6 +24,21 @@
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Company logo</label>
<p class="mt-1 text-xs text-slate-500">Shown on the visitor kiosk instead of the Ladill Frontdesk logo. PNG, JPG, WebP, or SVG up to 2&nbsp;MB.</p>
@if (\App\Support\OrganizationBranding::hasCustomLogo($organization))
<img src="{{ \App\Support\OrganizationBranding::logoUrl($organization) }}"
alt="{{ $organization->name }}"
class="mt-3 h-12 w-auto max-w-xs rounded-lg border border-slate-200 bg-white object-contain p-2">
<label class="mt-3 flex items-center gap-2 text-sm text-slate-600">
<input type="checkbox" name="remove_logo" value="1" @checked(old('remove_logo'))>
Remove custom logo
</label>
@endif
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/svg+xml"
class="mt-3 block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-indigo-700">
</div>
@else
<dl class="text-sm space-y-2">
<div class="flex justify-between"><dt class="text-slate-500">Name</dt><dd>{{ $organization->name }}</dd></div>
+15
View File
@@ -11,7 +11,9 @@ use App\Models\Organization;
use App\Models\User;
use App\Models\Visit;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class FrontdeskPhase7Test extends TestCase
@@ -93,6 +95,19 @@ class FrontdeskPhase7Test extends TestCase
$this->assertTrue($device->fresh()->isOnline());
}
public function test_kiosk_device_page_shows_custom_organization_logo(): void
{
Storage::fake('public');
$path = UploadedFile::fake()->image('brand.png', 320, 80)->store('frontdesk/organizations/'.$this->organization->id, 'public');
$this->organization->update(['logo_path' => $path]);
$device = $this->createKioskDevice();
$this->get(route('frontdesk.kiosk.device', $device->device_token))
->assertOk()
->assertSee(Storage::disk('public')->url($path), false);
}
public function test_kiosk_device_can_check_in_visitor(): void
{
$device = $this->createKioskDevice();
+44
View File
@@ -11,6 +11,8 @@ use App\Models\User;
use App\Models\Visit;
use App\Models\Visitor;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class FrontdeskWebTest extends TestCase
@@ -160,4 +162,46 @@ class FrontdeskWebTest extends TestCase
$this->assertEquals('Updated Org', $this->organization->name);
$this->assertEquals(6, $this->organization->settings['badge_expiry_hours']);
}
public function test_org_admin_can_upload_organization_logo(): void
{
Storage::fake('public');
$this->actingAs($this->user)
->put(route('frontdesk.settings.update'), [
'name' => 'Updated Org',
'timezone' => 'Africa/Accra',
'badge_expiry_hours' => 6,
'kiosk_reset_seconds' => 90,
'notification_channels' => ['email'],
'logo' => UploadedFile::fake()->image('company-logo.png', 400, 120),
])
->assertRedirect();
$this->organization->refresh();
$this->assertNotNull($this->organization->logo_path);
Storage::disk('public')->assertExists($this->organization->logo_path);
}
public function test_org_admin_can_remove_organization_logo(): void
{
Storage::fake('public');
$path = UploadedFile::fake()->image('company-logo.png')->store('frontdesk/organizations/'.$this->organization->id, 'public');
$this->organization->update(['logo_path' => $path]);
$this->actingAs($this->user)
->put(route('frontdesk.settings.update'), [
'name' => 'Updated Org',
'timezone' => 'Africa/Accra',
'badge_expiry_hours' => 6,
'kiosk_reset_seconds' => 90,
'notification_channels' => ['email'],
'remove_logo' => '1',
])
->assertRedirect();
$this->organization->refresh();
$this->assertNull($this->organization->logo_path);
Storage::disk('public')->assertMissing($path);
}
}