Implement multi-tenancy support with OAuth2 authentication for tenant admins, Stripe integration for event purchases and credits ledger, new Filament resources for event purchases, updated API routes and middleware for tenant isolation and token guarding, added factories/seeders/migrations for new models (Tenant, EventPurchase, OAuth entities, etc.), enhanced tests, and documentation updates. Removed outdated DemoAchievementsSeeder.

This commit is contained in:
2025-09-17 19:56:54 +02:00
parent 5fbb9cb240
commit 42d6e98dff
84 changed files with 6125 additions and 155 deletions

View File

@@ -0,0 +1,224 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\EventStoreRequest;
use Illuminate\Support\Str;
use App\Http\Resources\Tenant\EventResource;
use App\Models\Event;
use App\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
class EventController extends Controller
{
/**
* Display a listing of the tenant's events.
*/
public function index(Request $request): AnonymousResourceCollection
{
$tenantId = $request->attributes->get('tenant_id');
if (!$tenantId) {
throw ValidationException::withMessages([
'tenant_id' => 'Tenant ID not found in request context.',
]);
}
$query = Event::where('tenant_id', $tenantId)
->with(['eventType', 'photos'])
->orderBy('created_at', 'desc');
// Apply filters
if ($request->has('status')) {
$query->where('status', $request->status);
}
if ($request->has('type_id')) {
$query->where('event_type_id', $request->type_id);
}
// Pagination
$perPage = $request->get('per_page', 15);
$events = $query->paginate($perPage);
return EventResource::collection($events);
}
/**
* Store a newly created event in storage.
*/
public function store(EventStoreRequest $request): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
// Check credits balance
$tenant = Tenant::findOrFail($tenantId);
if ($tenant->event_credits_balance <= 0) {
return response()->json([
'error' => 'Insufficient event credits. Please purchase more credits.',
], 402);
}
$validated = $request->validated();
$event = Event::create(array_merge($validated, [
'tenant_id' => $tenantId,
'status' => 'draft', // Default status
'slug' => $this->generateUniqueSlug($validated['name'], $tenantId),
]));
// Decrement credits
$tenant->decrement('event_credits_balance', 1);
$event->load(['eventType', 'tenant']);
return response()->json([
'message' => 'Event created successfully',
'data' => new EventResource($event),
], 201);
}
/**
* Display the specified event.
*/
public function show(Request $request, Event $event): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
// Ensure event belongs to tenant
if ($event->tenant_id !== $tenantId) {
return response()->json(['error' => 'Event not found'], 404);
}
$event->load([
'eventType',
'photos' => fn ($query) => $query->with('likes')->latest(),
'tasks',
'tenant' => fn ($query) => $query->select('id', 'name', 'event_credits_balance')
]);
return response()->json([
'data' => new EventResource($event),
]);
}
/**
* Update the specified event in storage.
*/
public function update(EventStoreRequest $request, Event $event): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
// Ensure event belongs to tenant
if ($event->tenant_id !== $tenantId) {
return response()->json(['error' => 'Event not found'], 404);
}
$validated = $request->validated();
// Update slug if name changed
if ($validated['name'] !== $event->name) {
$validated['slug'] = $this->generateUniqueSlug($validated['name'], $tenantId, $event->id);
}
$event->update($validated);
$event->load(['eventType', 'tenant']);
return response()->json([
'message' => 'Event updated successfully',
'data' => new EventResource($event),
]);
}
/**
* Remove the specified event from storage.
*/
public function destroy(Request $request, Event $event): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
// Ensure event belongs to tenant
if ($event->tenant_id !== $tenantId) {
return response()->json(['error' => 'Event not found'], 404);
}
// Soft delete
$event->delete();
return response()->json([
'message' => 'Event deleted successfully',
]);
}
/**
* Bulk update event status (publish/unpublish)
*/
public function bulkUpdateStatus(Request $request): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$validated = $request->validate([
'event_ids' => 'required|array',
'event_ids.*' => 'exists:events,id',
'status' => 'required|in:draft,published,archived',
]);
$updatedCount = Event::whereIn('id', $validated['event_ids'])
->where('tenant_id', $tenantId)
->update(['status' => $validated['status']]);
return response()->json([
'message' => "{$updatedCount} events updated successfully",
'updated_count' => $updatedCount,
]);
}
/**
* Generate unique slug for event name
*/
private function generateUniqueSlug(string $name, int $tenantId, ?int $excludeId = null): string
{
$slug = Str::slug($name);
$originalSlug = $slug;
$counter = 1;
while (Event::where('slug', $slug)
->where('tenant_id', $tenantId)
->where('id', '!=', $excludeId)
->exists()) {
$slug = $originalSlug . '-' . $counter;
$counter++;
}
return $slug;
}
/**
* Search events by name or description
*/
public function search(Request $request): AnonymousResourceCollection
{
$tenantId = $request->attributes->get('tenant_id');
$query = $request->get('q', '');
if (strlen($query) < 2) {
return EventResource::collection(collect([]));
}
$events = Event::where('tenant_id', $tenantId)
->where(function ($q) use ($query) {
$q->where('name', 'like', "%{$query}%")
->orWhere('description', 'like', "%{$query}%");
})
->with('eventType')
->limit(10)
->get();
return EventResource::collection($events);
}
}

View File

@@ -0,0 +1,470 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\PhotoStoreRequest;
use App\Http\Resources\Tenant\PhotoResource;
use App\Models\Event;
use App\Models\Photo;
use App\Support\ImageHelper;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
class PhotoController extends Controller
{
/**
* Display a listing of the event's photos.
*/
public function index(Request $request, string $eventSlug): AnonymousResourceCollection
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$query = Photo::where('event_id', $event->id)
->with(['likes', 'uploader'])
->orderBy('created_at', 'desc');
// Filters
if ($request->has('status')) {
$query->where('status', $request->status);
}
if ($request->has('user_id')) {
$query->where('uploader_id', $request->user_id);
}
$perPage = $request->get('per_page', 20);
$photos = $query->paginate($perPage);
return PhotoResource::collection($photos);
}
/**
* Store a newly uploaded photo.
*/
public function store(PhotoStoreRequest $request, string $eventSlug): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$validated = $request->validated();
$file = $request->file('photo');
if (!$file) {
throw ValidationException::withMessages([
'photo' => 'No photo file uploaded.',
]);
}
// Validate file type and size
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($file->getMimeType(), $allowedTypes)) {
throw ValidationException::withMessages([
'photo' => 'Only JPEG, PNG, and WebP images are allowed.',
]);
}
if ($file->getSize() > 10 * 1024 * 1024) { // 10MB
throw ValidationException::withMessages([
'photo' => 'Photo size must be less than 10MB.',
]);
}
// Generate unique filename
$extension = $file->getClientOriginalExtension();
$filename = Str::uuid() . '.' . $extension;
$path = "events/{$eventSlug}/photos/{$filename}";
// Store original file
Storage::disk('public')->put($path, file_get_contents($file->getRealPath()));
// Generate thumbnail
$thumbnailPath = "events/{$eventSlug}/thumbnails/{$filename}";
$thumbnailRelative = ImageHelper::makeThumbnailOnDisk('public', $path, $thumbnailPath, 400);
if ($thumbnailRelative) {
$thumbnailPath = $thumbnailRelative;
}
// Create photo record
$photo = Photo::create([
'event_id' => $event->id,
'filename' => $filename,
'original_name' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'path' => $path,
'thumbnail_path' => $thumbnailPath,
'width' => null, // To be filled by image processing
'height' => null,
'status' => 'pending', // Requires moderation
'uploader_id' => $request->user()->id ?? null,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Get image dimensions
list($width, $height) = getimagesize($file->getRealPath());
$photo->update(['width' => $width, 'height' => $height]);
$photo->load(['event', 'uploader']);
return response()->json([
'message' => 'Photo uploaded successfully. Awaiting moderation.',
'data' => new PhotoResource($photo),
'moderation_notice' => 'Your photo has been uploaded and will be reviewed shortly.',
], 201);
}
/**
* Display the specified photo.
*/
public function show(Request $request, string $eventSlug, Photo $photo): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
}
$photo->load(['event', 'uploader', 'likes']);
$photo->increment('view_count');
return response()->json([
'data' => new PhotoResource($photo),
]);
}
/**
* Update the specified photo (moderation or metadata).
*/
public function update(Request $request, string $eventSlug, Photo $photo): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
}
$validated = $request->validate([
'status' => ['sometimes', 'in:pending,approved,rejected'],
'moderation_notes' => ['sometimes', 'required_if:status,rejected', 'string', 'max:1000'],
'caption' => ['sometimes', 'string', 'max:500'],
'alt_text' => ['sometimes', 'string', 'max:255'],
]);
// Only tenant admins can moderate
if (isset($validated['status']) && $request->user()->role !== 'admin') {
return response()->json(['error' => 'Insufficient permissions for moderation'], 403);
}
$photo->update($validated);
if ($validated['status'] ?? null === 'approved') {
$photo->load(['event', 'uploader']);
// Trigger event for new photo notification
// event(new \App\Events\PhotoApproved($photo)); // Implement later
}
return response()->json([
'message' => 'Photo updated successfully',
'data' => new PhotoResource($photo),
]);
}
/**
* Remove the specified photo from storage.
*/
public function destroy(Request $request, string $eventSlug, Photo $photo): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
}
// Delete from storage
Storage::disk('public')->delete([
$photo->path,
$photo->thumbnail_path,
]);
// Delete record and likes
DB::transaction(function () use ($photo) {
$photo->likes()->delete();
$photo->delete();
});
return response()->json([
'message' => 'Photo deleted successfully',
]);
}
/**
* Bulk approve photos (admin only)
*/
public function bulkApprove(Request $request, string $eventSlug): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$request->validate([
'photo_ids' => 'required|array',
'photo_ids.*' => 'exists:photos,id',
'moderation_notes' => 'nullable|string|max:1000',
]);
$photoIds = $request->photo_ids;
$approvedCount = Photo::whereIn('id', $photoIds)
->where('event_id', $event->id)
->where('status', 'pending')
->update([
'status' => 'approved',
'moderation_notes' => $request->moderation_notes,
'moderated_at' => now(),
'moderated_by' => $request->user()->id,
]);
// Load approved photos for response
$photos = Photo::whereIn('id', $photoIds)
->where('event_id', $event->id)
->with(['uploader', 'event'])
->get();
// Trigger events
foreach ($photos as $photo) {
// event(new \App\Events\PhotoApproved($photo)); // Implement later
}
return response()->json([
'message' => "{$approvedCount} photos approved successfully",
'approved_count' => $approvedCount,
'data' => PhotoResource::collection($photos),
]);
}
/**
* Bulk reject photos (admin only)
*/
public function bulkReject(Request $request, string $eventSlug): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$request->validate([
'photo_ids' => 'required|array',
'photo_ids.*' => 'exists:photos,id',
'moderation_notes' => 'required|string|max:1000',
]);
$photoIds = $request->photo_ids;
$rejectedCount = Photo::whereIn('id', $photoIds)
->where('event_id', $event->id)
->where('status', 'pending')
->update([
'status' => 'rejected',
'moderation_notes' => $request->moderation_notes,
'moderated_at' => now(),
'moderated_by' => $request->user()->id,
]);
// Optionally delete rejected photos from storage
$rejectedPhotos = Photo::whereIn('id', $photoIds)
->where('event_id', $event->id)
->get();
foreach ($rejectedPhotos as $photo) {
Storage::disk('public')->delete([
$photo->path,
$photo->thumbnail_path,
]);
}
return response()->json([
'message' => "{$rejectedCount} photos rejected and deleted",
'rejected_count' => $rejectedCount,
]);
}
/**
* Get photos for moderation (admin only)
*/
public function forModeration(Request $request, string $eventSlug): AnonymousResourceCollection
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$photos = Photo::where('event_id', $event->id)
->where('status', 'pending')
->with(['uploader', 'event'])
->orderBy('created_at', 'desc')
->paginate($request->get('per_page', 20));
return PhotoResource::collection($photos);
}
/**
* Get upload statistics for event
*/
public function stats(Request $request, string $eventSlug): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$totalPhotos = Photo::where('event_id', $event->id)->count();
$pendingPhotos = Photo::where('event_id', $event->id)->where('status', 'pending')->count();
$approvedPhotos = Photo::where('event_id', $event->id)->where('status', 'approved')->count();
$totalLikes = DB::table('photo_likes')->whereIn('photo_id',
Photo::where('event_id', $event->id)->pluck('id')
)->count();
$totalStorage = Photo::where('event_id', $event->id)->sum('size');
$uniqueUploaders = Photo::where('event_id', $event->id)
->select('uploader_id')
->distinct()
->count('uploader_id');
$recentUploads = Photo::where('event_id', $event->id)
->where('created_at', '>=', now()->subDays(7))
->count();
return response()->json([
'event_id' => $event->id,
'total_photos' => $totalPhotos,
'pending_photos' => $pendingPhotos,
'approved_photos' => $approvedPhotos,
'total_likes' => $totalLikes,
'total_storage_bytes' => $totalStorage,
'total_storage_mb' => round($totalStorage / (1024 * 1024), 2),
'unique_uploaders' => $uniqueUploaders,
'recent_uploads_7d' => $recentUploads,
'storage_quota_remaining' => $event->tenant->storage_quota - $totalStorage,
'quota_percentage_used' => min(100, round(($totalStorage / $event->tenant->storage_quota) * 100, 1)),
]);
}
/**
* Generate presigned S3 URL for direct upload (alternative to local storage)
*/
public function presignedUpload(Request $request, string $eventSlug): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$request->validate([
'filename' => 'required|string|max:255',
'content_type' => 'required|string|in:image/jpeg,image/png,image/webp',
]);
// Generate unique filename
$extension = pathinfo($request->filename, PATHINFO_EXTENSION);
$filename = Str::uuid() . '.' . $extension;
$path = "events/{$eventSlug}/pending/{$filename}";
// For local storage, return direct upload endpoint
// For S3, use Storage::disk('s3')->temporaryUrl() or presigned URL
$uploadUrl = url("/api/v1/tenant/events/{$eventSlug}/upload-direct");
$fields = [
'event_id' => $event->id,
'filename' => $filename,
'original_name' => $request->filename,
];
return response()->json([
'upload_url' => $uploadUrl,
'fields' => $fields,
'path' => $path,
'max_size' => 10 * 1024 * 1024, // 10MB
'allowed_types' => ['image/jpeg', 'image/png', 'image/webp'],
]);
}
/**
* Direct upload endpoint for presigned uploads
*/
public function uploadDirect(Request $request, string $eventSlug): JsonResponse
{
$tenantId = $request->attributes->get('tenant_id');
$event = Event::where('slug', $eventSlug)
->where('tenant_id', $tenantId)
->firstOrFail();
$request->validate([
'event_id' => 'required|exists:events,id',
'filename' => 'required|string',
'original_name' => 'required|string',
'photo' => 'required|image|mimes:jpeg,png,webp|max:10240', // 10MB
]);
if ($request->event_id !== $event->id) {
return response()->json(['error' => 'Invalid event ID'], 400);
}
$file = $request->file('photo');
$filename = $request->filename;
$path = "events/{$eventSlug}/photos/{$filename}";
// Store file
Storage::disk('public')->put($path, file_get_contents($file->getRealPath()));
// Generate thumbnail
$thumbnailPath = "events/{$eventSlug}/thumbnails/{$filename}";
$thumbnailRelative = ImageHelper::makeThumbnailOnDisk('public', $path, $thumbnailPath, 400);
if ($thumbnailRelative) {
$thumbnailPath = $thumbnailRelative;
}
// Create photo record
$photo = Photo::create([
'event_id' => $event->id,
'filename' => $filename,
'original_name' => $request->original_name,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'path' => $path,
'thumbnail_path' => $thumbnailPath,
'status' => 'pending',
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Get dimensions
list($width, $height) = getimagesize($file->getRealPath());
$photo->update(['width' => $width, 'height' => $height]);
return response()->json([
'message' => 'Upload successful. Awaiting moderation.',
'photo_id' => $photo->id,
'status' => 'pending',
], 201);
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\SettingsStoreRequest;
use App\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class SettingsController extends Controller
{
/**
* Get the tenant's settings.
*
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
$tenant = $request->tenant;
return response()->json([
'message' => 'Settings erfolgreich abgerufen.',
'data' => [
'id' => $tenant->id,
'settings' => $tenant->settings ?? [],
'updated_at' => $tenant->settings_updated_at?->toISOString(),
],
]);
}
/**
* Update the tenant's settings.
*
* @param SettingsStoreRequest $request
* @return JsonResponse
*/
public function update(SettingsStoreRequest $request): JsonResponse
{
$tenant = $request->tenant;
// Merge new settings with existing ones
$currentSettings = $tenant->settings ?? [];
$newSettings = array_merge($currentSettings, $request->validated()['settings']);
$tenant->update([
'settings' => $newSettings,
'settings_updated_at' => now(),
]);
return response()->json([
'message' => 'Settings erfolgreich aktualisiert.',
'data' => [
'id' => $tenant->id,
'settings' => $newSettings,
'updated_at' => now()->toISOString(),
],
]);
}
/**
* Reset tenant settings to defaults.
*
* @param Request $request
* @return JsonResponse
*/
public function reset(Request $request): JsonResponse
{
$tenant = $request->tenant;
$defaultSettings = [
'branding' => [
'logo_url' => null,
'primary_color' => '#3B82F6',
'secondary_color' => '#1F2937',
'font_family' => 'Inter, sans-serif',
],
'features' => [
'photo_likes_enabled' => true,
'event_checklist' => true,
'custom_domain' => false,
'advanced_analytics' => false,
],
'custom_domain' => null,
'contact_email' => $tenant->contact_email,
'event_default_type' => 'general',
];
$tenant->update([
'settings' => $defaultSettings,
'settings_updated_at' => now(),
]);
return response()->json([
'message' => 'Settings auf Standardwerte zurückgesetzt.',
'data' => [
'id' => $tenant->id,
'settings' => $defaultSettings,
'updated_at' => now()->toISOString(),
],
]);
}
/**
* Validate custom domain availability.
*
* @param Request $request
* @return JsonResponse
*/
public function validateDomain(Request $request): JsonResponse
{
$domain = $request->input('domain');
if (!$domain) {
return response()->json(['error' => 'Domain ist erforderlich.'], 400);
}
// Simple validation - in production, check DNS records or database uniqueness
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/', $domain)) {
return response()->json([
'available' => false,
'message' => 'Ungültiges Domain-Format.',
]);
}
// Check if domain is already taken by another tenant
$taken = Tenant::where('custom_domain', $domain)->where('id', '!=', $request->tenant->id)->exists();
return response()->json([
'available' => !$taken,
'message' => $taken ? 'Domain ist bereits vergeben.' : 'Domain ist verfügbar.',
]);
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\TaskStoreRequest;
use App\Http\Requests\Tenant\TaskUpdateRequest;
use App\Http\Resources\Tenant\TaskResource;
use App\Models\Task;
use App\Models\TaskCollection;
use App\Models\Event;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\JsonResponse;
class TaskController extends Controller
{
/**
* Display a listing of the tenant's tasks.
*
* @param Request $request
* @return AnonymousResourceCollection
*/
public function index(Request $request): AnonymousResourceCollection
{
$query = Task::where('tenant_id', $request->tenant->id)
->with(['taskCollection', 'assignedEvents'])
->orderBy('created_at', 'desc');
// Search and filters
if ($search = $request->get('search')) {
$query->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
}
if ($collectionId = $request->get('collection_id')) {
$query->whereHas('taskCollection', fn($q) => $q->where('id', $collectionId));
}
if ($eventId = $request->get('event_id')) {
$query->whereHas('assignedEvents', fn($q) => $q->where('id', $eventId));
}
$perPage = $request->get('per_page', 15);
$tasks = $query->paginate($perPage);
return TaskResource::collection($tasks);
}
/**
* Store a newly created task in storage.
*
* @param TaskStoreRequest $request
* @return JsonResponse
*/
public function store(TaskStoreRequest $request): JsonResponse
{
$task = Task::create(array_merge($request->validated(), [
'tenant_id' => $request->tenant->id,
]));
if ($collectionId = $request->input('collection_id')) {
$task->taskCollection()->associate(TaskCollection::findOrFail($collectionId));
$task->save();
}
$task->load(['taskCollection', 'assignedEvents']);
return response()->json([
'message' => 'Task erfolgreich erstellt.',
'data' => new TaskResource($task),
], 201);
}
/**
* Display the specified task.
*
* @param Request $request
* @param Task $task
* @return JsonResponse
*/
public function show(Request $request, Task $task): JsonResponse
{
if ($task->tenant_id !== $request->tenant->id) {
abort(404, 'Task nicht gefunden.');
}
$task->load(['taskCollection', 'assignedEvents']);
return response()->json(new TaskResource($task));
}
/**
* Update the specified task in storage.
*
* @param TaskUpdateRequest $request
* @param Task $task
* @return JsonResponse
*/
public function update(TaskUpdateRequest $request, Task $task): JsonResponse
{
if ($task->tenant_id !== $request->tenant->id) {
abort(404, 'Task nicht gefunden.');
}
$task->update($request->validated());
if ($collectionId = $request->input('collection_id')) {
$task->taskCollection()->associate(TaskCollection::findOrFail($collectionId));
$task->save();
}
$task->load(['taskCollection', 'assignedEvents']);
return response()->json([
'message' => 'Task erfolgreich aktualisiert.',
'data' => new TaskResource($task),
]);
}
/**
* Remove the specified task from storage.
*
* @param Request $request
* @param Task $task
* @return JsonResponse
*/
public function destroy(Request $request, Task $task): JsonResponse
{
if ($task->tenant_id !== $request->tenant->id) {
abort(404, 'Task nicht gefunden.');
}
$task->delete();
return response()->json([
'message' => 'Task erfolgreich gelöscht.',
]);
}
/**
* Assign task to an event.
*
* @param Request $request
* @param Task $task
* @param Event $event
* @return JsonResponse
*/
public function assignToEvent(Request $request, Task $task, Event $event): JsonResponse
{
if ($task->tenant_id !== $request->tenant->id || $event->tenant_id !== $request->tenant->id) {
abort(404);
}
if ($task->assignedEvents()->where('event_id', $event->id)->exists()) {
return response()->json(['message' => 'Task ist bereits diesem Event zugewiesen.'], 409);
}
$task->assignedEvents()->attach($event->id);
return response()->json([
'message' => 'Task erfolgreich dem Event zugewiesen.',
]);
}
/**
* Bulk assign tasks to an event.
*
* @param Request $request
* @param Event $event
* @return JsonResponse
*/
public function bulkAssignToEvent(Request $request, Event $event): JsonResponse
{
if ($event->tenant_id !== $request->tenant->id) {
abort(404);
}
$taskIds = $request->input('task_ids', []);
if (empty($taskIds)) {
return response()->json(['error' => 'Keine Task-IDs angegeben.'], 400);
}
$tasks = Task::whereIn('id', $taskIds)
->where('tenant_id', $request->tenant->id)
->get();
$attached = 0;
foreach ($tasks as $task) {
if (!$task->assignedEvents()->where('event_id', $event->id)->exists()) {
$task->assignedEvents()->attach($event->id);
$attached++;
}
}
return response()->json([
'message' => "{$attached} Tasks dem Event zugewiesen.",
]);
}
/**
* Get tasks for a specific event.
*
* @param Request $request
* @param Event $event
* @return AnonymousResourceCollection
*/
public function forEvent(Request $request, Event $event): AnonymousResourceCollection
{
if ($event->tenant_id !== $request->tenant->id) {
abort(404);
}
$tasks = Task::whereHas('assignedEvents', fn($q) => $q->where('event_id', $event->id))
->with(['taskCollection'])
->orderBy('created_at', 'desc')
->paginate($request->get('per_page', 15));
return TaskResource::collection($tasks);
}
/**
* Get tasks from a specific collection.
*
* @param Request $request
* @param TaskCollection $collection
* @return AnonymousResourceCollection
*/
public function fromCollection(Request $request, TaskCollection $collection): AnonymousResourceCollection
{
if ($collection->tenant_id !== $request->tenant->id) {
abort(404);
}
$tasks = $collection->tasks()
->with(['assignedEvents'])
->orderBy('created_at', 'desc')
->paginate($request->get('per_page', 15));
return TaskResource::collection($tasks);
}
}