Fix Event & EventType resource issues and apply formatting

- Fix EventType deletion error handling (constraint violations)
- Fix Event update error (package_id column missing)
- Fix Event Type dropdown options (JSON display issue)
- Fix EventPackagesRelationManager query error
- Add missing translations for deletion errors
- Apply Pint formatting
This commit is contained in:
Codex Agent
2026-01-21 10:34:06 +01:00
parent 198fbf6751
commit fa33e7cbcf
112 changed files with 334 additions and 280 deletions

View File

@@ -7,7 +7,6 @@ use App\Models\Tenant;
use App\Models\User;
use Illuminate\Console\Attributes\AsCommand;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
#[AsCommand(name: 'tenant:attach-demo-event')]
class AttachDemoEvent extends Command
@@ -25,10 +24,12 @@ class AttachDemoEvent extends Command
{
if (! \Illuminate\Support\Facades\Schema::hasTable('events')) {
$this->error("Table 'events' does not exist. Run: php artisan migrate");
return self::FAILURE;
}
if (! \Illuminate\Support\Facades\Schema::hasColumn('events', 'tenant_id')) {
$this->error("Column 'events.tenant_id' does not exist. Add it and rerun. Suggested: create a migration to add a nullable foreignId to tenants.");
return self::FAILURE;
}
$tenant = null;
@@ -45,6 +46,7 @@ class AttachDemoEvent extends Command
}
if (! $tenant) {
$this->error('Tenant not found. Provide --tenant-slug or a user with tenant_id via --tenant-email.');
return self::FAILURE;
}
@@ -67,12 +69,14 @@ class AttachDemoEvent extends Command
if (! $event) {
$this->error('Event not found. Provide --event-id or --event-slug.');
return self::FAILURE;
}
// Idempotent update
if ((int) $event->tenant_id === (int) $tenant->id) {
$this->info("Event #{$event->id} already attached to tenant #{$tenant->id} ({$tenant->slug}).");
return self::SUCCESS;
}
@@ -80,6 +84,7 @@ class AttachDemoEvent extends Command
$event->save();
$this->info("Attached event #{$event->id} ({$event->slug}) to tenant #{$tenant->id} ({$tenant->slug}).");
return self::SUCCESS;
}
}

View File

@@ -10,22 +10,27 @@ use Illuminate\Support\Facades\Storage;
class BackfillThumbnails extends Command
{
protected $signature = 'media:backfill-thumbnails {--limit=500}';
protected $description = 'Generate thumbnails for photos missing thumbnail_path or where thumbnail equals original.';
public function handle(): int
{
$limit = (int) $this->option('limit');
$rows = DB::table('photos')
->select(['id','event_id','file_path','thumbnail_path'])
->select(['id', 'event_id', 'file_path', 'thumbnail_path'])
->orderBy('id')
->limit($limit)
->get();
$count = 0;
foreach ($rows as $r) {
$orig = $this->relativeFromUrl((string)$r->file_path);
$thumb = (string)($r->thumbnail_path ?? '');
if ($thumb && $thumb !== $r->file_path) continue; // already set to different thumb
if (! $orig) continue;
$orig = $this->relativeFromUrl((string) $r->file_path);
$thumb = (string) ($r->thumbnail_path ?? '');
if ($thumb && $thumb !== $r->file_path) {
continue;
} // already set to different thumb
if (! $orig) {
continue;
}
$baseName = pathinfo($orig, PATHINFO_FILENAME);
$destRel = "events/{$r->event_id}/photos/thumbs/{$baseName}_thumb.jpg";
$made = ImageHelper::makeThumbnailOnDisk('public', $orig, $destRel, 640, 82);
@@ -39,6 +44,7 @@ class BackfillThumbnails extends Command
}
}
$this->info("Done. Thumbnails generated: {$count}");
return self::SUCCESS;
}
@@ -49,6 +55,7 @@ class BackfillThumbnails extends Command
if (str_starts_with($p, '/storage/')) {
return substr($p, strlen('/storage/'));
}
return null;
}
}

View File

@@ -4,15 +4,15 @@ namespace App\Console\Commands;
use App\Models\PackagePurchase;
use App\Models\Tenant;
use App\Models\User;
use App\Models\TenantPackage;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
class MigrateLegacyPurchases extends Command
{
protected $signature = 'packages:migrate-legacy';
protected $description = 'Migrate legacy purchases to new system with temp tenants';
public function handle()
@@ -21,19 +21,20 @@ class MigrateLegacyPurchases extends Command
if ($legacyPurchases->isEmpty()) {
$this->info('No legacy purchases found.');
return 0;
}
$this->info("Found {$legacyPurchases->count()} legacy purchases.");
foreach ($legacyPurchases as $purchase) {
if (!$purchase->user_id) {
if (! $purchase->user_id) {
// Create temp user if no user
$tempUser = User::create([
'name' => 'Legacy User ' . $purchase->id,
'email' => 'legacy' . $purchase->id . '@fotospiel.local',
'name' => 'Legacy User '.$purchase->id,
'email' => 'legacy'.$purchase->id.'@fotospiel.local',
'password' => Hash::make('legacy'),
'username' => 'legacy' . $purchase->id,
'username' => 'legacy'.$purchase->id,
'first_name' => 'Legacy',
'last_name' => 'User',
'address' => 'Legacy Address',
@@ -43,7 +44,7 @@ class MigrateLegacyPurchases extends Command
$tempTenant = Tenant::create([
'user_id' => $tempUser->id,
'name' => 'Legacy Tenant ' . $purchase->id,
'name' => 'Legacy Tenant '.$purchase->id,
'status' => 'active',
]);
@@ -73,6 +74,7 @@ class MigrateLegacyPurchases extends Command
}
$this->info('Legacy migration completed.');
return 0;
}
}
}

View File

@@ -62,7 +62,7 @@ class SendAbandonedCheckoutReminders extends Command
if ($this->shouldSendReminder($checkout, $stage)) {
$resumeUrl = $this->generateResumeUrl($checkout);
if (!$isDryRun) {
if (! $isDryRun) {
$mailLocale = $checkout->user->preferred_locale ?? config('app.locale');
Mail::to($checkout->user)
@@ -86,8 +86,8 @@ class SendAbandonedCheckoutReminders extends Command
$totalProcessed++;
}
} catch (Throwable $e) {
Log::error("Failed to send {$stage} reminder for checkout {$checkout->id}: " . $e->getMessage());
$this->error(" ❌ Failed to process checkout {$checkout->id}: " . $e->getMessage());
Log::error("Failed to send {$stage} reminder for checkout {$checkout->id}: ".$e->getMessage());
$this->error(" ❌ Failed to process checkout {$checkout->id}: ".$e->getMessage());
}
}
}
@@ -98,7 +98,7 @@ class SendAbandonedCheckoutReminders extends Command
->count();
if ($oldCheckouts > 0) {
if (!$isDryRun) {
if (! $isDryRun) {
AbandonedCheckoutModel::where('abandoned_at', '<', now()->subDays(30))
->where('converted', false)
->delete();
@@ -108,10 +108,10 @@ class SendAbandonedCheckoutReminders extends Command
}
}
$this->info("✅ Reminder process completed!");
$this->info('✅ Reminder process completed!');
$this->info(" Processed: {$totalProcessed} checkouts");
if (!$isDryRun) {
if (! $isDryRun) {
$this->info(" Sent: {$totalSent} reminder emails");
} else {
$this->info(" Would send: {$totalSent} reminder emails");
@@ -131,12 +131,12 @@ class SendAbandonedCheckoutReminders extends Command
}
// User existiert noch?
if (!$checkout->user) {
if (! $checkout->user) {
return false;
}
// Package existiert noch?
if (!$checkout->package) {
if (! $checkout->package) {
return false;
}

View File

@@ -6,4 +6,4 @@ enum PackageType: string
{
case ENDCUSTOMER = 'endcustomer';
case RESELLER = 'reseller';
}
}

View File

@@ -5,8 +5,6 @@ namespace App\Exports;
use App\Models\EventPurchase;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class EventPurchaseExporter extends Exporter
{
@@ -28,11 +26,10 @@ class EventPurchaseExporter extends Exporter
];
}
public static function getCompletedNotificationBody(Export $export): string
{
$body = "Your Event Purchases export has completed and is ready for download. {$export->successful_rows} purchases were exported.";
return $body;
}
}
}

View File

@@ -16,4 +16,4 @@ class ListCategories extends ListRecords
Actions\CreateAction::make(),
];
}
}
}

View File

@@ -16,4 +16,4 @@ class ListPosts extends ListRecords
Actions\CreateAction::make(),
];
}
}
}

View File

@@ -8,4 +8,4 @@ use Filament\Resources\Pages\ViewRecord;
class ViewPost extends ViewRecord
{
protected static string $resource = PostResource::class;
}
}

View File

@@ -26,4 +26,4 @@ trait HasContentEditor
'h3',
]));
}
}
}

View File

@@ -13,7 +13,9 @@ use Illuminate\Support\Facades\Storage;
class ImportEmotions extends Page
{
protected static string $resource = EmotionResource::class;
protected string $view = 'filament.resources.emotion-resource.pages.import-emotions';
protected ?string $heading = null;
public ?string $file = null;
@@ -36,6 +38,7 @@ class ImportEmotions extends Page
$path = $this->form->getState()['file'] ?? null;
if (! $path || ! Storage::disk('public')->exists($path)) {
Notification::make()->danger()->title(__('admin.notifications.file_not_found'))->send();
return;
}

View File

@@ -16,4 +16,4 @@ class ListEventPurchases extends ListRecords
Actions\CreateAction::make(),
];
}
}
}

View File

@@ -65,14 +65,15 @@ class EventResource extends Resource
->required(),
Select::make('event_type_id')
->label(__('admin.events.fields.type'))
->options(EventType::query()->pluck('name', 'id'))
->options(fn () => EventType::all()->pluck('name.de', 'id'))
->searchable(),
Select::make('package_id')
->label(__('admin.events.fields.package'))
->options(\App\Models\Package::query()->where('type', 'endcustomer')->pluck('name', 'id'))
->searchable()
->preload()
->required(),
->required()
->visibleOn('create'),
TextInput::make('default_locale')
->label(__('admin.events.fields.default_locale'))
->default('de')

View File

@@ -8,4 +8,25 @@ use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreateEvent extends AuditedCreateRecord
{
protected static string $resource = EventResource::class;
public ?int $packageId = null;
protected function mutateFormDataBeforeCreate(array $data): array
{
$this->packageId = $data['package_id'] ?? null;
unset($data['package_id']);
return $data;
}
protected function afterCreate(): void
{
if ($this->packageId) {
$this->record->eventPackages()->create([
'package_id' => $this->packageId,
]);
}
parent::afterCreate();
}
}

View File

@@ -19,7 +19,6 @@ use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
class EventPackagesRelationManager extends RelationManager
{
@@ -59,6 +58,7 @@ class EventPackagesRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn (Builder $query) => $query->with('package'))
->recordTitleAttribute('package.name')
->columns([
TextColumn::make('package.name')
@@ -147,9 +147,4 @@ class EventPackagesRelationManager extends RelationManager
{
return __('admin.events.relation_managers.event_packages.title');
}
public function getTableQuery(): Builder|Relation
{
return parent::getTableQuery()->with('package');
}
}

View File

@@ -113,18 +113,64 @@ class EventTypeResource extends Resource
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\DeleteAction::make()
->action(function (EventType $record, Actions\DeleteAction $action) {
try {
$record->delete();
} catch (\Exception $e) {
$isConstraint = ($e instanceof \Illuminate\Database\QueryException && ($e->getCode() == 23000 || ($e->errorInfo[0] ?? '') == 23000));
if ($isConstraint) {
\Filament\Notifications\Notification::make()
->title(__('admin.common.error'))
->body(__('admin.event_types.messages.delete_constraint_error'))
->danger()
->send();
$action->halt();
}
throw $e;
}
})
->after(fn (EventType $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
])
->bulkActions([
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
->action(function (Collection $records, Actions\DeleteBulkAction $action) {
$logger = app(SuperAdminAuditLogger::class);
$deletedCount = 0;
$failedCount = 0;
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
try {
$record->delete();
$logger->recordModelMutation('deleted', $record, source: static::class);
$deletedCount++;
} catch (\Exception $e) {
$isConstraint = ($e instanceof \Illuminate\Database\QueryException && ($e->getCode() == 23000 || ($e->errorInfo[0] ?? '') == 23000));
if ($isConstraint) {
$failedCount++;
} else {
throw $e;
}
}
}
if ($failedCount > 0) {
\Filament\Notifications\Notification::make()
->title(__('admin.common.error'))
->body(__('admin.event_types.messages.delete_constraint_error')." ($failedCount failed, $deletedCount deleted)")
->danger()
->send();
if ($deletedCount === 0) {
$action->halt();
}
}
}),
]);

View File

@@ -17,4 +17,3 @@ class ListMediaStorageTargets extends ListRecords
];
}
}

View File

@@ -16,4 +16,4 @@ class ListPackages extends ListRecords
Actions\CreateAction::make(),
];
}
}
}

View File

@@ -14,4 +14,3 @@ class ListPurchaseHistories extends ListRecords
return [];
}
}

View File

@@ -14,4 +14,3 @@ class ViewPurchaseHistory extends ViewRecord
return [];
}
}

View File

@@ -16,4 +16,4 @@ class ListPurchases extends ListRecords
Actions\CreateAction::make(),
];
}
}
}

View File

@@ -3,8 +3,8 @@
namespace App\Filament\SuperAdmin\Pages\Auth;
use Filament\Auth\Pages\EditProfile as BaseEditProfile;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Log;
@@ -38,4 +38,4 @@ class EditProfile extends BaseEditProfile
$this->getCurrentPasswordFormComponent(),
]);
}
}
}

View File

@@ -4,8 +4,8 @@ namespace App\Filament\Widgets;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class PlatformStatsWidget extends BaseWidget
{

View File

@@ -7,7 +7,6 @@ use Filament\Widgets\LineChartWidget;
class RevenueTrendWidget extends LineChartWidget
{
protected static ?int $sort = 1;
protected int|string|array $columnSpan = 'full';

View File

@@ -2,9 +2,9 @@
namespace App\Filament\Widgets;
use App\Models\Tenant;
use Filament\Tables;
use Filament\Widgets\TableWidget as BaseWidget;
use App\Models\Tenant;
class TopTenantsByUploads extends BaseWidget
{
@@ -14,6 +14,7 @@ class TopTenantsByUploads extends BaseWidget
{
return __('admin.widgets.top_tenants_by_uploads.heading');
}
protected ?string $pollingInterval = '60s';
public function table(Tables\Table $table): Tables\Table
@@ -33,4 +34,3 @@ class TopTenantsByUploads extends BaseWidget
->paginated(false);
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Http\Controllers\Admin;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
class QrController extends BaseController
@@ -15,7 +15,7 @@ class QrController extends BaseController
return response('missing data', 400);
}
$png = QrCode::format('png')->size(300)->generate($data);
return response($png, 200, ['Content-Type' => 'image/png']);
}
}

View File

@@ -26,11 +26,11 @@ class LegalController extends BaseController
'allow_unsafe_links' => false,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);
$environment->addExtension(new AutolinkExtension);
$environment->addExtension(new StrikethroughExtension);
$environment->addExtension(new TaskListExtension);
$this->markdown = new MarkdownConverter($environment);
}

View File

@@ -7,7 +7,6 @@ use App\Models\Event;
use App\Services\Analytics\EventAnalyticsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class EventAnalyticsController extends Controller
{
@@ -23,13 +22,13 @@ class EventAnalyticsController extends Controller
if (is_string($packageFeatures)) {
$packageFeatures = json_decode($packageFeatures, true) ?? [];
}
$hasAccess = in_array('advanced_analytics', $packageFeatures, true);
if (!$hasAccess) {
return response()->json([
if (! $hasAccess) {
return response()->json([
'message' => 'This feature is only available in the Premium package.',
'code' => 'feature_locked'
'code' => 'feature_locked',
], 403);
}

View File

@@ -112,4 +112,3 @@ class FontController extends Controller
return $fonts;
}
}

View File

@@ -91,14 +91,14 @@ class PhotoboothController extends Controller
$recipientName = $user->fullName ?? $user->name ?? $user->email;
$mail = (new PhotoboothUploaderDownload(
recipientName: $recipientName,
eventName: $eventName,
links: [
'windows' => url('/downloads/PhotoboothUploader-win-x64.exe'),
'macos' => url('/downloads/PhotoboothUploader-macos-x64'),
'linux' => url('/downloads/PhotoboothUploader-linux-x64'),
],
))->locale($locale);
recipientName: $recipientName,
eventName: $eventName,
links: [
'windows' => url('/downloads/PhotoboothUploader-win-x64.exe'),
'macos' => url('/downloads/PhotoboothUploader-macos-x64'),
'linux' => url('/downloads/PhotoboothUploader-linux-x64'),
],
))->locale($locale);
Mail::to($user->email)->queue($mail);

View File

@@ -69,7 +69,7 @@ class LegalPageController extends Controller
$effectiveFrom = optional($page->effective_from);
return Inertia::render('legal/Show', [
'seoTitle' => $title . ' - ' . config('app.name', 'Fotospiel'),
'seoTitle' => $title.' - '.config('app.name', 'Fotospiel'),
'title' => $title,
'content' => $this->convertMarkdownToHtml($bodyMarkdown),
'effectiveFrom' => $effectiveFrom ? $effectiveFrom->toDateString() : null,
@@ -112,11 +112,11 @@ class LegalPageController extends Controller
'allow_unsafe_links' => false,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);
$environment->addExtension(new AutolinkExtension);
$environment->addExtension(new StrikethroughExtension);
$environment->addExtension(new TaskListExtension);
$converter = new MarkdownConverter($environment);

View File

@@ -35,7 +35,7 @@ class EventPhotoArchiveController extends Controller
abort(404, 'No approved photos available for this event.');
}
$zip = new ZipArchive();
$zip = new ZipArchive;
$tempPath = tempnam(sys_get_temp_dir(), 'fotospiel-photos-');
if ($tempPath === false || $zip->open($tempPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
@@ -129,4 +129,3 @@ class EventPhotoArchiveController extends Controller
return false;
}
}

View File

@@ -21,7 +21,7 @@ class SetLocale
$sessionLocale = Session::get('locale', 'de');
// Fallback to Accept-Language header if no session
if (!in_array($sessionLocale, $supportedLocales)) {
if (! in_array($sessionLocale, $supportedLocales)) {
$acceptLanguage = $request->header('Accept-Language', 'de');
$localeFromHeader = substr($acceptLanguage, 0, 2);
$sessionLocale = in_array($localeFromHeader, $supportedLocales) ? $localeFromHeader : 'de';

View File

@@ -2,11 +2,11 @@
namespace App\Http\Middleware;
use App\Support\LocaleConfig;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Session;
use App\Support\LocaleConfig;
class SetLocaleFromRequest
{

View File

@@ -19,4 +19,3 @@ class SetLocaleFromUser
return $next($request);
}
}

View File

@@ -57,8 +57,3 @@ class ProfileUpdateRequest extends FormRequest
];
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Http\Requests\Tenant;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PhotoStoreRequest extends FormRequest
{
@@ -59,4 +58,4 @@ class PhotoStoreRequest extends FormRequest
'tags' => $this->tags ? explode(',', $this->tags) : [],
]);
}
}
}

View File

@@ -88,4 +88,3 @@ class SettingsStoreRequest extends FormRequest
]);
}
}

View File

@@ -18,4 +18,4 @@ class CreditLedgerResource extends JsonResource
'created_at' => $this->created_at->toISOString(),
];
}
}
}

View File

@@ -59,6 +59,7 @@ class EmotionResource extends JsonResource
}
$first = reset($value);
return $first !== false ? (string) $first : $fallback;
}
}

View File

@@ -21,4 +21,4 @@ class EventPurchaseResource extends JsonResource
'created_at' => $this->created_at->toISOString(),
];
}
}
}

View File

@@ -31,6 +31,7 @@ class ArchiveEventMediaAssets implements ShouldQueue
if (! $event) {
Log::warning('Archive job aborted: event missing', ['event_id' => $this->eventId]);
return;
}
@@ -38,6 +39,7 @@ class ArchiveEventMediaAssets implements ShouldQueue
if (! $archiveDisk) {
Log::warning('Archive job aborted: no archive disk configured', ['event_id' => $event->id]);
return;
}

View File

@@ -2,12 +2,12 @@
namespace App\Jobs\Concerns;
use App\Models\Tenant;
use App\Models\TenantNotificationLog;
use App\Models\TenantNotificationReceipt;
use App\Models\Tenant;
use App\Services\Packages\TenantNotificationLogger;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
trait LogsTenantNotifications
{

View File

@@ -26,7 +26,7 @@ class AbandonedCheckout extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: __('emails.abandoned_checkout.subject_' . $this->timing, [
subject: __('emails.abandoned_checkout.subject_'.$this->timing, [
'package' => $this->localizedPackageName(),
]),
);

View File

@@ -13,9 +13,7 @@ class ContactConfirmation extends Mailable
use Queueable;
use SerializesModels;
public function __construct(public string $name)
{
}
public function __construct(public string $name) {}
public function envelope(): Envelope
{

View File

@@ -75,11 +75,11 @@ class AbandonedCheckout extends Model
public function scopeReadyForReminder($query, string $stage, int $hours)
{
return $query->where('reminder_stage', '!=', $stage)
->where('reminder_stage', '!=', 'converted')
->where('abandoned_at', '<=', now()->subHours($hours))
->where(function ($q) {
$q->whereNull('reminded_at')
->orWhere('reminded_at', '<=', now()->subHours(24));
});
->where('reminder_stage', '!=', 'converted')
->where('abandoned_at', '<=', now()->subHours($hours))
->where(function ($q) {
$q->whereNull('reminded_at')
->orWhere('reminded_at', '<=', now()->subHours(24));
});
}
}

View File

@@ -19,4 +19,4 @@ class BlogAuthor extends Model
'github_handle',
'twitter_handle',
];
}
}

View File

@@ -7,7 +7,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Log;
class BlogCategory extends Model
{
@@ -42,4 +41,4 @@ class BlogCategory extends Model
{
return $query->where('is_visible', false);
}
}
}

View File

@@ -8,19 +8,18 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Spatie\Translatable\HasTranslations;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
use Spatie\Translatable\HasTranslations;
class BlogPost extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
use HasFactory, HasTranslations, SoftDeletes;
protected $table = 'blog_posts';
@@ -78,14 +77,15 @@ class BlogPost extends Model
return Attribute::get(function () {
$markdown = $this->getTranslation('content', app()->getLocale());
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TaskListExtension());
$environment = new Environment;
$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);
$environment->addExtension(new AutolinkExtension);
$environment->addExtension(new StrikethroughExtension);
$environment->addExtension(new TaskListExtension);
$converter = new MarkdownConverter($environment);
return (string) $converter->convert($markdown);
});
}

View File

@@ -3,14 +3,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Tags\Tag;
use Spatie\Translatable\HasTranslations;
class BlogTag extends Tag
{
use HasFactory, SoftDeletes, HasTranslations;
use HasFactory, HasTranslations, SoftDeletes;
protected $translatable = [
'name',
@@ -20,4 +19,4 @@ class BlogTag extends Tag
'slug',
'translations',
];
}
}

View File

@@ -13,7 +13,9 @@ class Emotion extends Model
use HasFactory;
protected $table = 'emotions';
protected $guarded = [];
protected $casts = [
'name' => 'array',
'description' => 'array',
@@ -34,4 +36,3 @@ class Emotion extends Model
return $this->hasMany(Task::class);
}
}

View File

@@ -47,4 +47,3 @@ class EventJoinTokenEvent extends Model
return $this->belongsTo(Tenant::class);
}
}

View File

@@ -51,4 +51,3 @@ class EventMediaAsset extends Model
return $this->belongsTo(Photo::class);
}
}

View File

@@ -11,7 +11,9 @@ class EventPurchase extends Model
use HasFactory;
protected $table = 'event_purchases';
protected $guarded = [];
protected $casts = [
'purchased_at' => 'datetime',
'amount' => 'decimal:2',
@@ -21,4 +23,4 @@ class EventPurchase extends Model
{
return $this->belongsTo(Tenant::class);
}
}
}

View File

@@ -36,4 +36,3 @@ class EventStorageAssignment extends Model
return $this->belongsTo(MediaStorageTarget::class, 'media_storage_target_id');
}
}

View File

@@ -12,7 +12,9 @@ class EventType extends Model
use HasFactory;
protected $table = 'event_types';
protected $guarded = [];
protected $casts = [
'name' => 'array',
'settings' => 'array',
@@ -28,4 +30,3 @@ class EventType extends Model
return $this->hasMany(Event::class);
}
}

View File

@@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model;
class LegalPage extends Model
{
protected $table = 'legal_pages';
protected $guarded = [];
protected $casts = [
'title' => 'array',
'body_markdown' => 'array',
@@ -15,4 +17,3 @@ class LegalPage extends Model
'effective_from' => 'datetime',
];
}

View File

@@ -63,4 +63,3 @@ class MediaStorageTarget extends Model
return array_merge($base, $config);
}
}

View File

@@ -8,11 +8,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PhotoLike extends Model
{
protected $table = 'photo_likes';
protected $guarded = [];
public $timestamps = false;
public function photo(): BelongsTo
{
return $this->belongsTo(Photo::class);
}
}
}

View File

@@ -11,8 +11,11 @@ class PurchaseHistory extends Model
use HasFactory;
protected $table = 'purchase_history';
public $timestamps = false;
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
@@ -28,4 +31,3 @@ class PurchaseHistory extends Model
return $this->belongsTo(Tenant::class);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Services\Analytics;
use App\Models\Event;
use App\Models\Photo;
use Illuminate\Support\Facades\DB;
class EventAnalyticsService
@@ -17,7 +16,7 @@ class EventAnalyticsService
// Adjust for timezone if necessary, but for now we'll use UTC or server time
// Ideally we should use the event's timezone if stored, or client's.
// We'll return data in ISO format buckets.
$stats = $event->photos()
->selectRaw('DATE_FORMAT(created_at, "%Y-%m-%d %H:00:00") as hour, count(*) as count')
->groupBy('hour')

View File

@@ -5,7 +5,6 @@ namespace App\Services\Analytics;
use App\Models\EventJoinToken;
use App\Models\EventJoinTokenEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class JoinTokenAnalyticsRecorder
@@ -109,4 +108,3 @@ class JoinTokenAnalyticsRecorder
return Str::substr($token, 0, 6).'…'.Str::substr($token, -4);
}
}

View File

@@ -19,6 +19,7 @@ class EmotionImportService
$headers = fgetcsv($handle, 0, ',');
if (! $headers) {
fclose($handle);
return [0, 0];
}

View File

@@ -192,4 +192,3 @@ class PhotoSecurityScanner
}
}
}

View File

@@ -8,8 +8,6 @@ use App\Models\EventStorageAssignment;
use App\Models\MediaStorageTarget;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class EventStorageManager
{

View File

@@ -60,4 +60,3 @@ class StorageHealthService
}
}
}

View File

@@ -9,6 +9,7 @@ class LocaleConfig
{
/**
* Return configured locales from env/config as provided (may include region codes).
*
* @return array<int, string>
*/
public static function configured(): array
@@ -34,6 +35,7 @@ class LocaleConfig
/**
* Return normalized short codes (language only, lowercase).
*
* @return array<int, string>
*/
public static function normalized(): array