feat(tenant): add site title and favicon support for tenants and website types

This commit is contained in:
2026-08-14 08:55:59 -03:00
parent 4de08ff2f2
commit bfdaf1c38b
17 changed files with 355 additions and 6 deletions

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('website_type', function (Blueprint $table): void {
$table->string('site_title')->nullable()->after('scanner_domain');
$table->unsignedBigInteger('favicon_id')->nullable()->after('footer_logo');
$table->foreign('favicon_id')
->references('id')
->on('attachments')
->nullOnDelete();
});
Schema::table('tenants', function (Blueprint $table): void {
$table->string('site_title')->nullable()->after('dominio');
$table->unsignedBigInteger('favicon_id')->nullable()->after('footer_logo_id');
$table->foreign('favicon_id')
->references('id')
->on('attachments')
->nullOnDelete();
});
DB::table('website_type')
->whereNull('site_title')
->update(['site_title' => DB::raw('nombre')]);
}
public function down(): void
{
Schema::table('tenants', function (Blueprint $table): void {
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
$table->dropForeign(['favicon_id']);
}
$table->dropColumn(['site_title', 'favicon_id']);
});
Schema::table('website_type', function (Blueprint $table): void {
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
$table->dropForeign(['favicon_id']);
}
$table->dropColumn(['site_title', 'favicon_id']);
});
}
};

View File

@@ -0,0 +1,134 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
return new class extends Migration
{
/** @var array<string, array{title: string, image: string, filename: string}> */
private const BRANDING = [
'fiesta_futbol_infantil' => [
'title' => 'Fiesta Fútbol Infantil',
'image' => 'images/tennants/fiesta_futbol_infantil/futbol_infantil_favicon.png',
'filename' => 'futbol_infantil_favicon.png',
],
'desfile_pura_tendencia' => [
'title' => 'Desfile Pura Tendencia',
'image' => 'images/tennants/desfile_pura_tendencia/pura_tendencia_favicon.png',
'filename' => 'pura_tendencia_favicon.png',
],
];
/** @var list<string> */
private array $storedPaths = [];
public function up(): void
{
try {
DB::transaction(function (): void {
foreach (self::BRANDING as $tenantCode => $branding) {
$tenant = DB::table('tenants')
->where('codigo', $tenantCode)
->first(['id', 'favicon_id']);
if ($tenant === null) {
continue;
}
$faviconId = $tenant->favicon_id;
$currentFilename = $faviconId === null
? null
: DB::table('attachments')->where('id', $faviconId)->value('filename');
if ($currentFilename !== $branding['filename']) {
$faviconId = $this->storeFavicon(
$tenantCode,
$branding['image'],
$branding['filename'],
);
}
DB::table('tenants')
->where('id', $tenant->id)
->update([
'site_title' => $branding['title'],
'favicon_id' => $faviconId,
'updated_at' => now(),
]);
}
});
} catch (Throwable $throwable) {
Storage::disk('s3')->delete($this->storedPaths);
throw $throwable;
}
}
public function down(): void
{
foreach (self::BRANDING as $tenantCode => $branding) {
$tenant = DB::table('tenants')
->where('codigo', $tenantCode)
->first(['id', 'site_title', 'favicon_id']);
if ($tenant === null) {
continue;
}
$favicon = $tenant->favicon_id === null
? null
: DB::table('attachments')->where('id', $tenant->favicon_id)->first();
DB::table('tenants')
->where('id', $tenant->id)
->update([
'site_title' => $tenant->site_title === $branding['title'] ? null : $tenant->site_title,
'favicon_id' => $favicon?->filename === $branding['filename'] ? null : $tenant->favicon_id,
'updated_at' => now(),
]);
if ($favicon?->filename === $branding['filename']) {
Storage::disk('s3')->delete($favicon->path);
DB::table('attachments')->where('id', $favicon->id)->delete();
}
}
}
private function storeFavicon(string $tenantCode, string $relativePath, string $filename): int
{
$sourcePath = public_path($relativePath);
if (! is_file($sourcePath)) {
throw new RuntimeException("Favicon not found at path: {$sourcePath}");
}
$contents = file_get_contents($sourcePath);
if ($contents === false) {
throw new RuntimeException("Could not read favicon at path: {$sourcePath}");
}
$key = (string) Str::uuid();
$storedPath = "tenants/{$tenantCode}/{$key}.png";
if (! Storage::disk('s3')->put($storedPath, $contents)) {
throw new RuntimeException("Could not store favicon at path: {$storedPath}");
}
$this->storedPaths[] = $storedPath;
return DB::table('attachments')->insertGetId([
'key' => $key,
'path' => $storedPath,
'filename' => $filename,
'type' => 'image',
'mime_type' => 'image/png',
'extension' => 'png',
'size' => strlen($contents),
'created_at' => now(),
'updated_at' => now(),
]);
}
};

View File

@@ -104,6 +104,7 @@ class TenantSeeder extends Seeder
'codigo' => 'fiesta_futbol_infantil',
'nombre' => 'Fiesta Fútbol Infantil',
'dominio' => $fiestaDomain,
'site_title' => 'Fiesta Fútbol Infantil',
'primary_color' => '#00973F',
'secondary_color' => '#A0A0A0',
'danger_color' => '#FF8888',
@@ -121,6 +122,10 @@ class TenantSeeder extends Seeder
'images/tennants/fiesta_futbol_infantil/futbol_infantil_footer.png',
'futbol_infantil_footer.png',
),
'favicon' => $this->uploadedImage(
'images/tennants/fiesta_futbol_infantil/futbol_infantil_favicon.png',
'futbol_infantil_favicon.png',
),
'social_media' => self::SOCIAL_MEDIA,
'website_type_code' => 'onticket',
'extras' => [
@@ -229,6 +234,18 @@ class TenantSeeder extends Seeder
}
}
if (
$tenant->favicon
&& $tenant->favicon_id !== $tenant->header_logo_id
&& $tenant->favicon_id !== $tenant->footer_logo_id
) {
try {
$attachmentService->delete($tenant->favicon);
} catch (Throwable) {
// Ignore cleanup errors while recreating demo data.
}
}
$tenant->delete();
}

View File

@@ -32,6 +32,7 @@ class WebsiteTypeSeeder extends Seeder
'nombre' => 'ShopIt',
'dominio' => 'localhost',
'scanner_domain' => 'scanner.localhost',
'site_title' => 'ShopIt',
...self::PRESENTATION,
'site_logo' => $this->onTicketLogo(),
'footer_logo' => $this->onTicketFooterLogo(),
@@ -65,6 +66,7 @@ class WebsiteTypeSeeder extends Seeder
'nombre' => 'OnTicket',
'dominio' => 'onticket.localhost',
'scanner_domain' => 'scanner.onticket.localhost',
'site_title' => 'OnTicket',
...self::PRESENTATION,
'site_logo' => $this->onTicketLogo(),
'footer_logo' => $this->onTicketFooterLogo(),