added watermark settings tab on the branding page and added more package details to the billing page, added a new guest notifications page

This commit is contained in:
Codex Agent
2025-12-17 16:39:25 +01:00
parent efe697f155
commit 5f3e7ae8c8
25 changed files with 2062 additions and 202 deletions

View File

@@ -5,9 +5,8 @@ namespace App\Filament\SuperAdmin\Pages;
use App\Models\WatermarkSetting;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Pages\Page;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Storage;
use Filament\Pages\Page;
class WatermarkSettingsPage extends Page
{
@@ -20,11 +19,19 @@ class WatermarkSettingsPage extends Page
protected static ?int $navigationSort = 20;
public ?string $asset = null;
public string $position = 'bottom-right';
public float $opacity = 0.25;
public float $scale = 0.2;
public int $padding = 16;
public int $offset_x = 0;
public int $offset_y = 0;
public function mount(): void
{
$settings = WatermarkSetting::query()->first();
@@ -35,6 +42,8 @@ class WatermarkSettingsPage extends Page
$this->opacity = (float) $settings->opacity;
$this->scale = (float) $settings->scale;
$this->padding = (int) $settings->padding;
$this->offset_x = (int) ($settings->offset_x ?? 0);
$this->offset_y = (int) ($settings->offset_y ?? 0);
}
}
@@ -80,6 +89,20 @@ class WatermarkSettingsPage extends Page
->minValue(0)
->default(16)
->required(),
Forms\Components\TextInput::make('offset_x')
->label('Offset X (px)')
->numeric()
->minValue(-500)
->maxValue(500)
->default(0)
->required(),
Forms\Components\TextInput::make('offset_y')
->label('Offset Y (px)')
->numeric()
->minValue(-500)
->maxValue(500)
->default(0)
->required(),
])->columns(2);
}
@@ -93,6 +116,8 @@ class WatermarkSettingsPage extends Page
$settings->opacity = $this->opacity;
$settings->scale = $this->scale;
$settings->padding = $this->padding;
$settings->offset_x = $this->offset_x;
$settings->offset_y = $this->offset_y;
$settings->save();
Notification::make()

View File

@@ -21,6 +21,7 @@ use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -140,6 +141,7 @@ class EventController extends Controller
}
$settings['branding_allowed'] = $package->branding_allowed !== false;
$settings['watermark_allowed'] = $package->watermark_allowed !== false;
$eventData['settings'] = $settings;
@@ -258,7 +260,9 @@ class EventController extends Controller
unset($validated[$unused]);
}
$brandingAllowed = optional($event->eventPackage?->package)->branding_allowed !== false;
$package = $event->eventPackage?->package;
$brandingAllowed = optional($package)->branding_allowed !== false;
$watermarkAllowed = optional($package)->watermark_allowed !== false;
if (isset($validated['settings']) && is_array($validated['settings'])) {
$validated['settings'] = array_merge($event->settings ?? [], $validated['settings']);
@@ -267,6 +271,92 @@ class EventController extends Controller
}
$validated['settings']['branding_allowed'] = $brandingAllowed;
$validated['settings']['watermark_allowed'] = $watermarkAllowed;
$settings = $validated['settings'];
$watermark = Arr::get($settings, 'watermark', []);
$existingWatermark = is_array($watermark) ? $watermark : [];
if (is_array($watermark)) {
$mode = $watermark['mode'] ?? 'base';
$policy = $watermarkAllowed ? 'basic' : 'none';
if (! $watermarkAllowed) {
$mode = 'off';
} elseif (! $brandingAllowed) {
$mode = 'base';
} elseif ($mode === 'off' && $policy === 'basic') {
$mode = 'base';
}
$assetPath = $watermark['asset'] ?? null;
$assetDataUrl = $watermark['asset_data_url'] ?? null;
if (! $watermarkAllowed) {
$assetPath = null;
}
if ($assetDataUrl && $mode === 'custom' && $brandingAllowed) {
if (! preg_match('/^data:image\\/(png|webp|jpe?g);base64,(.+)$/i', $assetDataUrl, $matches)) {
throw ValidationException::withMessages([
'settings.watermark.asset_data_url' => __('Ungültiges Wasserzeichen-Bild.'),
]);
}
$decoded = base64_decode($matches[2], true);
if ($decoded === false) {
throw ValidationException::withMessages([
'settings.watermark.asset_data_url' => __('Wasserzeichen konnte nicht gelesen werden.'),
]);
}
if (strlen($decoded) > 3 * 1024 * 1024) { // 3 MB
throw ValidationException::withMessages([
'settings.watermark.asset_data_url' => __('Wasserzeichen ist zu groß (max. 3 MB).'),
]);
}
$extension = str_starts_with(strtolower($matches[1]), 'jp') ? 'jpg' : strtolower($matches[1]);
$path = sprintf('branding/watermarks/event-%s.%s', $event->id, $extension);
Storage::disk('public')->put($path, $decoded);
$assetPath = $path;
}
$position = $watermark['position'] ?? 'bottom-right';
$validPositions = [
'top-left',
'top-center',
'top-right',
'middle-left',
'center',
'middle-right',
'bottom-left',
'bottom-center',
'bottom-right',
];
if (! in_array($position, $validPositions, true)) {
$position = 'bottom-right';
}
$settings['watermark'] = [
'mode' => $mode,
'asset' => $assetPath,
'position' => $position,
'opacity' => isset($watermark['opacity']) ? (float) $watermark['opacity'] : ($existingWatermark['opacity'] ?? null),
'scale' => isset($watermark['scale']) ? (float) $watermark['scale'] : ($existingWatermark['scale'] ?? null),
'padding' => isset($watermark['padding']) ? (int) $watermark['padding'] : ($existingWatermark['padding'] ?? null),
'offset_x' => isset($watermark['offset_x']) ? (int) $watermark['offset_x'] : ($existingWatermark['offset_x'] ?? 0),
'offset_y' => isset($watermark['offset_y']) ? (int) $watermark['offset_y'] : ($existingWatermark['offset_y'] ?? 0),
];
}
if (array_key_exists('watermark_serve_originals', $settings)) {
$settings['watermark_serve_originals'] = (bool) $settings['watermark_serve_originals'];
}
$validated['settings'] = $settings;
$event->update($validated);
$event->load(['eventType', 'tenant']);

View File

@@ -30,8 +30,16 @@ class TenantPackageController extends Controller
->get();
$packages->each(function ($package) {
$package->remaining_events = $package->package->max_events_per_year - $package->used_events;
$package->package_limits = $package->package->getAttributes(); // Or custom accessor for limits
$pkg = $package->package;
$package->remaining_events = $pkg->max_events_per_year - $package->used_events;
$package->package_limits = array_merge(
$pkg->limits,
[
'branding_allowed' => $pkg->branding_allowed,
'watermark_allowed' => $pkg->watermark_allowed,
'features' => $pkg->features,
]
);
});
return response()->json([

View File

@@ -46,6 +46,27 @@ class EventStoreRequest extends FormRequest
'settings.branding.*' => ['nullable'],
'settings.engagement_mode' => ['nullable', Rule::in(['tasks', 'photo_only'])],
'settings.guest_upload_visibility' => ['nullable', Rule::in(['review', 'immediate'])],
'settings.watermark' => ['nullable', 'array'],
'settings.watermark.mode' => ['nullable', Rule::in(['base', 'custom', 'off'])],
'settings.watermark.asset' => ['nullable', 'string', 'max:500'],
'settings.watermark.asset_data_url' => ['nullable', 'string'],
'settings.watermark.position' => ['nullable', Rule::in([
'top-left',
'top-center',
'top-right',
'middle-left',
'center',
'middle-right',
'bottom-left',
'bottom-center',
'bottom-right',
])],
'settings.watermark.opacity' => ['nullable', 'numeric', 'min:0', 'max:1'],
'settings.watermark.scale' => ['nullable', 'numeric', 'min:0.05', 'max:1'],
'settings.watermark.padding' => ['nullable', 'integer', 'min:0', 'max:500'],
'settings.watermark.offset_x' => ['nullable', 'integer', 'min:-500', 'max:500'],
'settings.watermark.offset_y' => ['nullable', 'integer', 'min:-500', 'max:500'],
'settings.watermark_serve_originals' => ['nullable', 'boolean'],
];
}

View File

@@ -79,6 +79,8 @@ class EventResource extends JsonResource
'price' => $eventPackage->purchased_price,
'purchased_at' => $eventPackage->purchased_at?->toIso8601String(),
'expires_at' => $eventPackage->gallery_expires_at?->toIso8601String(),
'branding_allowed' => (bool) optional($eventPackage->package)->branding_allowed,
'watermark_allowed' => (bool) optional($eventPackage->package)->watermark_allowed,
] : null,
'limits' => $eventPackage && $limitEvaluator
? $limitEvaluator->summarizeEventPackage($eventPackage)

View File

@@ -15,5 +15,7 @@ class WatermarkSetting extends Model
'opacity',
'scale',
'padding',
'offset_x',
'offset_y',
];
}

View File

@@ -33,6 +33,7 @@ class ImageHelper
$h = imagesy($src);
if ($w === 0 || $h === 0) {
imagedestroy($src);
return null;
}
@@ -138,18 +139,44 @@ class ImageHelper
$x = $padding;
$y = $padding;
if ($position === 'top-right') {
$x = max(0, $srcW - $targetW - $padding);
} elseif ($position === 'bottom-left') {
$y = max(0, $srcH - $targetH - $padding);
} elseif ($position === 'bottom-right') {
$x = max(0, $srcW - $targetW - $padding);
$y = max(0, $srcH - $targetH - $padding);
} elseif ($position === 'center') {
$x = (int) max(0, ($srcW - $targetW) / 2);
$y = (int) max(0, ($srcH - $targetH) / 2);
switch ($position) {
case 'top-right':
$x = max(0, $srcW - $targetW - $padding);
break;
case 'top-center':
$x = (int) max(0, ($srcW - $targetW) / 2);
break;
case 'middle-left':
$y = (int) max(0, ($srcH - $targetH) / 2);
break;
case 'center':
$x = (int) max(0, ($srcW - $targetW) / 2);
$y = (int) max(0, ($srcH - $targetH) / 2);
break;
case 'middle-right':
$x = max(0, $srcW - $targetW - $padding);
$y = (int) max(0, ($srcH - $targetH) / 2);
break;
case 'bottom-left':
$y = max(0, $srcH - $targetH - $padding);
break;
case 'bottom-center':
$x = (int) max(0, ($srcW - $targetW) / 2);
$y = max(0, $srcH - $targetH - $padding);
break;
case 'bottom-right':
$x = max(0, $srcW - $targetW - $padding);
$y = max(0, $srcH - $targetH - $padding);
break;
default:
break;
}
$offsetX = (int) ($config['offset_x'] ?? 0);
$offsetY = (int) ($config['offset_y'] ?? 0);
$x = max(0, min($srcW - $targetW, $x + $offsetX));
$y = max(0, min($srcH - $targetH, $y + $offsetY));
$opacity = max(0.0, min(1.0, (float) ($config['opacity'] ?? 0.25)));
$mergeOpacity = (int) round($opacity * 100); // imagecopymerge uses 0-100

View File

@@ -33,7 +33,7 @@ class WatermarkConfigResolver
}
/**
* @return array{type:string, policy:string, asset?:string, position?:string, opacity?:float, scale?:float, padding?:int}
* @return array{type:string, policy:string, asset?:string, position?:string, opacity?:float, scale?:float, padding?:int, offset_x?:int, offset_y?:int}
*/
public static function resolve(Event $event): array
{
@@ -61,6 +61,8 @@ class WatermarkConfigResolver
'opacity' => $baseSetting?->opacity ?? config('watermark.base.opacity', 0.25),
'scale' => $baseSetting?->scale ?? config('watermark.base.scale', 0.2),
'padding' => $baseSetting?->padding ?? config('watermark.base.padding', 16),
'offset_x' => $baseSetting?->offset_x ?? config('watermark.base.offset_x', 0),
'offset_y' => $baseSetting?->offset_y ?? config('watermark.base.offset_y', 0),
];
$event->loadMissing('eventPackage.package', 'tenant');
@@ -91,6 +93,8 @@ class WatermarkConfigResolver
$opacity = (float) ($source['opacity'] ?? $base['opacity'] ?? 0.25);
$scale = (float) ($source['scale'] ?? $base['scale'] ?? 0.2);
$padding = (int) ($source['padding'] ?? $base['padding'] ?? 16);
$offsetX = (int) ($source['offset_x'] ?? $base['offset_x'] ?? 0);
$offsetY = (int) ($source['offset_y'] ?? $base['offset_y'] ?? 0);
$clamp = static fn (float $value, float $min, float $max) => max($min, min($max, $value));
@@ -102,6 +106,8 @@ class WatermarkConfigResolver
'opacity' => $clamp($opacity, 0.0, 1.0),
'scale' => $clamp($scale, 0.05, 1.0),
'padding' => max(0, $padding),
'offset_x' => max(-500, min(500, $offsetX)),
'offset_y' => max(-500, min(500, $offsetY)),
'serve_originals' => $serveOriginals,
];
}