feat(addons): finalize event addon catalog and ai styling upgrade flow
This commit is contained in:
@@ -5,14 +5,20 @@ namespace App\Filament\Resources;
|
||||
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
|
||||
use App\Filament\Resources\PackageAddonResource\Pages;
|
||||
use App\Jobs\SyncPackageAddonToLemonSqueezy;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\PackageAddon;
|
||||
use App\Services\Audit\SuperAdminAuditLogger;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
@@ -53,7 +59,15 @@ class PackageAddonResource extends Resource
|
||||
TextInput::make('variant_id')
|
||||
->label('Lemon Squeezy Variant-ID')
|
||||
->helperText('Variant-ID aus Lemon Squeezy für dieses Add-on')
|
||||
->required(fn (Get $get): bool => (bool) $get('active') && ! is_numeric($get('metadata.price_eur')))
|
||||
->maxLength(191),
|
||||
TextInput::make('metadata.price_eur')
|
||||
->label('PayPal Preis (EUR)')
|
||||
->helperText('Für PayPal-Checkout erforderlich (z. B. 9.90).')
|
||||
->numeric()
|
||||
->step(0.01)
|
||||
->minValue(0.01)
|
||||
->required(fn (Get $get): bool => (bool) $get('active') && blank($get('variant_id'))),
|
||||
TextInput::make('sort')
|
||||
->label('Sortierung')
|
||||
->numeric()
|
||||
@@ -61,6 +75,23 @@ class PackageAddonResource extends Resource
|
||||
Toggle::make('active')
|
||||
->label('Aktiv')
|
||||
->default(true),
|
||||
Placeholder::make('sellable_state')
|
||||
->label('Verfügbarkeits-Check')
|
||||
->content(function (Get $get): string {
|
||||
$isActive = (bool) $get('active');
|
||||
$hasVariant = filled($get('variant_id'));
|
||||
$hasPayPalPrice = is_numeric($get('metadata.price_eur'));
|
||||
|
||||
if (! $isActive) {
|
||||
return 'Inaktiv';
|
||||
}
|
||||
|
||||
if (! $hasVariant && ! $hasPayPalPrice) {
|
||||
return 'Nicht verkäuflich: Variant-ID oder PayPal Preis fehlt.';
|
||||
}
|
||||
|
||||
return 'Verkäuflich';
|
||||
}),
|
||||
]),
|
||||
Section::make('Limits-Inkremente')
|
||||
->columns(3)
|
||||
@@ -81,6 +112,30 @@ class PackageAddonResource extends Resource
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
]),
|
||||
Section::make('Feature-Entitlements')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('metadata.scope')
|
||||
->label('Scope')
|
||||
->options([
|
||||
'photos' => 'Fotos',
|
||||
'guests' => 'Gäste',
|
||||
'gallery' => 'Galerie',
|
||||
'feature' => 'Feature',
|
||||
'bundle' => 'Bundle',
|
||||
])
|
||||
->native(false)
|
||||
->searchable(),
|
||||
TagsInput::make('metadata.entitlements.features')
|
||||
->label('Freigeschaltete Features')
|
||||
->helperText('Feature-Keys für Freischaltungen, z. B. ai_styling')
|
||||
->placeholder('z. B. ai_styling')
|
||||
->columnSpanFull(),
|
||||
DateTimePicker::make('metadata.entitlements.expires_at')
|
||||
->label('Entitlement gültig bis')
|
||||
->seconds(false)
|
||||
->nullable(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -100,6 +155,29 @@ class PackageAddonResource extends Resource
|
||||
->label('Lemon Squeezy Variant-ID')
|
||||
->toggleable()
|
||||
->copyable(),
|
||||
TextColumn::make('metadata.price_eur')
|
||||
->label('PayPal Preis (EUR)')
|
||||
->formatStateUsing(fn (mixed $state): string => is_numeric($state) ? number_format((float) $state, 2, ',', '.').' €' : '—')
|
||||
->toggleable(),
|
||||
TextColumn::make('metadata.scope')
|
||||
->label('Scope')
|
||||
->badge()
|
||||
->toggleable(),
|
||||
TextColumn::make('metadata.entitlements.features')
|
||||
->label('Features')
|
||||
->formatStateUsing(function (mixed $state): string {
|
||||
if (! is_array($state)) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$features = array_values(array_filter(array_map(
|
||||
static fn (mixed $feature): string => trim((string) $feature),
|
||||
$state,
|
||||
)));
|
||||
|
||||
return $features === [] ? '—' : implode(', ', $features);
|
||||
})
|
||||
->toggleable(),
|
||||
TextColumn::make('extra_photos')->label('Fotos +'),
|
||||
TextColumn::make('extra_guests')->label('Gäste +'),
|
||||
TextColumn::make('extra_gallery_days')->label('Galerietage +'),
|
||||
@@ -110,6 +188,14 @@ class PackageAddonResource extends Resource
|
||||
'danger' => false,
|
||||
])
|
||||
->formatStateUsing(fn (bool $state) => $state ? 'Aktiv' : 'Inaktiv'),
|
||||
BadgeColumn::make('sellability')
|
||||
->label('Checkout')
|
||||
->state(fn (PackageAddon $record): string => static::sellabilityLabel($record))
|
||||
->colors([
|
||||
'success' => fn (string $state): bool => $state === 'Verkäuflich',
|
||||
'warning' => fn (string $state): bool => $state === 'Unvollständig',
|
||||
'gray' => fn (string $state): bool => $state === 'Inaktiv',
|
||||
]),
|
||||
TextColumn::make('sort')
|
||||
->label('Sort')
|
||||
->sortable()
|
||||
@@ -166,4 +252,21 @@ class PackageAddonResource extends Resource
|
||||
'edit' => Pages\EditPackageAddon::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function sellabilityLabel(PackageAddon $record): string
|
||||
{
|
||||
if (! $record->active) {
|
||||
return 'Inaktiv';
|
||||
}
|
||||
|
||||
return $record->isSellableForProvider(static::addonProvider()) ? 'Verkäuflich' : 'Unvollständig';
|
||||
}
|
||||
|
||||
protected static function addonProvider(): string
|
||||
{
|
||||
return (string) (
|
||||
config('package-addons.provider')
|
||||
?? config('checkout.default_provider', CheckoutSession::PROVIDER_PAYPAL)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,9 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
|
||||
use App\Filament\Resources\PackageResource\Pages;
|
||||
use App\Jobs\PullPackageFromLemonSqueezy;
|
||||
use App\Jobs\SyncPackageToLemonSqueezy;
|
||||
use App\Models\Package;
|
||||
use App\Services\Audit\SuperAdminAuditLogger;
|
||||
use BackedEnum;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
@@ -26,7 +23,6 @@ use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs as SchemaTabs;
|
||||
@@ -301,71 +297,6 @@ class PackageResource extends Resource
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->actions([
|
||||
Actions\Action::make('syncLemonSqueezy')
|
||||
->label('Mit Lemon Squeezy abgleichen')
|
||||
->icon('heroicon-o-cloud-arrow-up')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->disabled(fn (Package $record) => $record->lemonsqueezy_sync_status === 'syncing')
|
||||
->action(function (Package $record) {
|
||||
SyncPackageToLemonSqueezy::dispatch($record->id);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Lemon Squeezy-Sync gestartet')
|
||||
->body('Das Paket wird im Hintergrund mit Lemon Squeezy abgeglichen.')
|
||||
->send();
|
||||
}),
|
||||
Actions\Action::make('linkLemonSqueezy')
|
||||
->label('Lemon Squeezy verknüpfen')
|
||||
->icon('heroicon-o-link')
|
||||
->color('info')
|
||||
->form([
|
||||
TextInput::make('lemonsqueezy_product_id')
|
||||
->label('Lemon Squeezy Produkt-ID')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
TextInput::make('lemonsqueezy_variant_id')
|
||||
->label('Lemon Squeezy Variant-ID')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
])
|
||||
->fillForm(fn (Package $record) => [
|
||||
'lemonsqueezy_product_id' => $record->lemonsqueezy_product_id,
|
||||
'lemonsqueezy_variant_id' => $record->lemonsqueezy_variant_id,
|
||||
])
|
||||
->action(function (Package $record, array $data): void {
|
||||
$record->linkLemonSqueezyIds($data['lemonsqueezy_product_id'], $data['lemonsqueezy_variant_id']);
|
||||
|
||||
PullPackageFromLemonSqueezy::dispatch($record->id);
|
||||
|
||||
app(SuperAdminAuditLogger::class)->recordModelMutation(
|
||||
'linked',
|
||||
$record,
|
||||
SuperAdminAuditLogger::fieldsMetadata($data),
|
||||
static::class
|
||||
);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Lemon Squeezy-Verknüpfung gespeichert')
|
||||
->body('Die IDs wurden gespeichert und ein Pull wurde angestoßen.')
|
||||
->send();
|
||||
}),
|
||||
Actions\Action::make('pullLemonSqueezy')
|
||||
->label('Status von Lemon Squeezy holen')
|
||||
->icon('heroicon-o-cloud-arrow-down')
|
||||
->disabled(fn (Package $record) => ! $record->lemonsqueezy_product_id && ! $record->lemonsqueezy_variant_id)
|
||||
->requiresConfirmation()
|
||||
->action(function (Package $record) {
|
||||
PullPackageFromLemonSqueezy::dispatch($record->id);
|
||||
|
||||
Notification::make()
|
||||
->info()
|
||||
->title('Lemon Squeezy-Abgleich angefordert')
|
||||
->body('Der aktuelle Stand aus Lemon Squeezy wird geladen und hier hinterlegt.')
|
||||
->send();
|
||||
}),
|
||||
ViewAction::make(),
|
||||
EditAction::make()
|
||||
->after(fn (array $data, Package $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
|
||||
|
||||
Reference in New Issue
Block a user