Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).

Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
Codex Agent
2025-11-01 19:50:17 +01:00
parent 2c14493604
commit 79b209de9a
55 changed files with 3348 additions and 462 deletions

View File

@@ -22,6 +22,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
class PhotoController extends Controller
{
@@ -46,27 +47,16 @@ class PhotoController extends Controller
->where('tenant_id', $tenantId)
->firstOrFail();
$event->loadMissing(['tenant', 'eventPackage.package', 'eventPackages.package']);
$tenant = $event->tenant;
if ($tenant) {
$violation = $this->packageLimitEvaluator->assessPhotoUpload($tenant, $event->id, $event);
if ($violation !== null) {
return ApiError::response(
$violation['code'],
$violation['title'],
$violation['message'],
$violation['status'],
$violation['meta']
);
}
}
$eventPackage = $tenant
? $this->packageLimitEvaluator->resolveEventPackageForPhotoUpload($tenant, $event->id, $event)
: null;
$previousUsedPhotos = $eventPackage?->used_photos ?? 0;
$limitSummary = $eventPackage
? $this->packageLimitEvaluator->summarizeEventPackage($eventPackage)
: null;
$query = Photo::where('event_id', $event->id)
->with('event')->withCount('likes')
@@ -84,7 +74,9 @@ class PhotoController extends Controller
$perPage = $request->get('per_page', 20);
$photos = $query->paginate($perPage);
return PhotoResource::collection($photos);
return PhotoResource::collection($photos)->additional([
'limits' => $limitSummary,
]);
}
/**
@@ -97,6 +89,29 @@ class PhotoController extends Controller
->where('tenant_id', $tenantId)
->firstOrFail();
$event->loadMissing(['tenant', 'eventPackage.package', 'eventPackages.package']);
$tenant = $event->tenant;
$eventPackage = $tenant
? $this->packageLimitEvaluator->resolveEventPackageForPhotoUpload($tenant, $event->id, $event)
: null;
if ($tenant) {
$violation = $this->packageLimitEvaluator->assessPhotoUpload($tenant, $event->id, $event);
if ($violation !== null) {
return ApiError::response(
$violation['code'],
$violation['title'],
$violation['message'],
$violation['status'],
$violation['meta']
);
}
}
$previousUsedPhotos = $eventPackage?->used_photos ?? 0;
$validated = $request->validated();
$file = $request->file('photo');
@@ -197,12 +212,17 @@ class PhotoController extends Controller
$this->packageUsageTracker->recordPhotoUsage($eventPackage, $previousUsedPhotos, 1);
}
$limitSummary = $eventPackage
? $this->packageLimitEvaluator->summarizeEventPackage($eventPackage)
: null;
$photo->load('event')->loadCount('likes');
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.',
'limits' => $limitSummary,
], 201);
}
@@ -217,7 +237,13 @@ class PhotoController extends Controller
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
return ApiError::response(
'photo_not_found',
'Foto nicht gefunden',
'Das Foto gehört nicht zu diesem Event.',
404,
['photo_id' => $photo->id]
);
}
$photo->load('event')->loadCount('likes');
@@ -239,7 +265,13 @@ class PhotoController extends Controller
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
return ApiError::response(
'photo_not_found',
'Foto nicht gefunden',
'Das Foto gehört nicht zu diesem Event.',
404,
['photo_id' => $photo->id]
);
}
$validated = $request->validate([
@@ -251,7 +283,13 @@ class PhotoController extends Controller
// Only tenant admins can moderate
if (isset($validated['status']) && ! $this->tokenHasScope($request, 'tenant:write')) {
return response()->json(['error' => 'Insufficient scopes'], 403);
return ApiError::response(
'insufficient_scope',
'Insufficient Scopes',
'You are not allowed to moderate photos for this event.',
Response::HTTP_FORBIDDEN,
['required_scope' => 'tenant:write']
);
}
$photo->update($validated);
@@ -279,7 +317,13 @@ class PhotoController extends Controller
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
return ApiError::response(
'photo_not_found',
'Foto nicht gefunden',
'Das Foto gehört nicht zu diesem Event.',
404,
['photo_id' => $photo->id]
);
}
$assets = EventMediaAsset::where('photo_id', $photo->id)->get();
@@ -303,6 +347,9 @@ class PhotoController extends Controller
Storage::disk($fallbackDisk)->delete([$photo->path, $photo->thumbnail_path]);
}
$eventPackage = $event->eventPackage;
$usageTracker = app(\App\Services\Packages\PackageUsageTracker::class);
// Delete record and likes
DB::transaction(function () use ($photo, $assets) {
$photo->likes()->delete();
@@ -312,6 +359,15 @@ class PhotoController extends Controller
$photo->delete();
});
if ($eventPackage && $eventPackage->package) {
$previousUsed = (int) $eventPackage->used_photos;
if ($previousUsed > 0) {
$eventPackage->decrement('used_photos');
$eventPackage->refresh();
$usageTracker->recordPhotoUsage($eventPackage, $previousUsed, -1);
}
}
return response()->json([
'message' => 'Photo deleted successfully',
]);
@@ -328,7 +384,13 @@ class PhotoController extends Controller
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
return ApiError::response(
'photo_not_found',
'Photo not found',
'The specified photo could not be located for this event.',
Response::HTTP_NOT_FOUND,
['photo_id' => $photo->id, 'event_id' => $event->id]
);
}
$photo->update(['is_featured' => true]);
@@ -345,7 +407,13 @@ class PhotoController extends Controller
->firstOrFail();
if ($photo->event_id !== $event->id) {
return response()->json(['error' => 'Photo not found'], 404);
return ApiError::response(
'photo_not_found',
'Photo not found',
'The specified photo could not be located for this event.',
Response::HTTP_NOT_FOUND,
['photo_id' => $photo->id, 'event_id' => $event->id]
);
}
$photo->update(['is_featured' => false]);
@@ -569,7 +637,13 @@ class PhotoController extends Controller
]);
if ($request->event_id !== $event->id) {
return response()->json(['error' => 'Invalid event ID'], 400);
return ApiError::response(
'event_mismatch',
'Invalid Event',
'The provided event does not match the authenticated tenant event.',
Response::HTTP_BAD_REQUEST,
['payload_event_id' => $request->event_id, 'expected_event_id' => $event->id]
);
}
$event->load('storageAssignments.storageTarget');