feat(service): keyvisual/background upload + scope choice

This commit is contained in:
Thorsten Bus 2026-05-31 00:15:08 +02:00
parent 1ce30b76e3
commit b31f21959f
3 changed files with 210 additions and 0 deletions

View file

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers;
use App\Models\Service;
use App\Models\Setting;
use App\Services\FileConversionService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class ServiceImageController extends Controller
{
public function storeKeyVisual(Request $request, Service $service): RedirectResponse
{
return $this->store($request, $service, 'key_visual_filename', 'current_key_visual');
}
public function storeBackground(Request $request, Service $service): RedirectResponse
{
return $this->store($request, $service, 'background_filename', 'current_background');
}
private function store(Request $request, Service $service, string $column, string $settingKey): RedirectResponse
{
$request->validate([
'file' => ['required', 'file', 'mimes:jpg,jpeg,png', 'max:20480'],
'scope' => ['required', Rule::in(['service', 'default'])],
], [
'file.required' => 'Bitte wähle eine Bilddatei aus.',
'file.file' => 'Die hochgeladene Datei ist ungültig.',
'file.mimes' => 'Nur Bilddateien (jpg, png) sind erlaubt.',
'file.max' => 'Die Datei darf maximal 20 MB groß sein.',
'scope.required' => 'Bitte wähle einen Geltungsbereich.',
'scope.in' => 'Der gewählte Geltungsbereich ist ungültig.',
]);
$result = app(FileConversionService::class)->convertImageCover($request->file('file'));
$service->update([$column => $result['filename']]);
if ($request->input('scope') === 'default') {
Setting::set($settingKey, $result['filename']);
}
return back()->with('success', 'Bild wurde gespeichert.');
}
}

View file

@ -8,6 +8,7 @@
use App\Http\Controllers\MacroAssignmentController;
use App\Http\Controllers\MacroImportController;
use App\Http\Controllers\ServiceController;
use App\Http\Controllers\ServiceImageController;
use App\Http\Controllers\ServiceMacroOverrideController;
use App\Http\Controllers\SettingsController;
use App\Http\Controllers\SongPdfController;
@ -67,6 +68,8 @@
Route::get('/services/{service}/download-bundle/{blockType}', [ServiceController::class, 'downloadBundle'])->name('services.download-bundle');
Route::get('/services/{service}/agenda-items/{agendaItem}/download', [ServiceController::class, 'downloadAgendaItem'])->name('services.agenda-item.download');
Route::get('/services/{service}/edit', [ServiceController::class, 'edit'])->name('services.edit');
Route::post('/services/{service}/key-visual', [ServiceImageController::class, 'storeKeyVisual'])->name('services.key-visual.store');
Route::post('/services/{service}/background', [ServiceImageController::class, 'storeBackground'])->name('services.background.store');
Route::get('/songs/{song}/translate', [TranslationController::class, 'page'])->name('songs.translate');
Route::get('/songs', function () {

View file

@ -0,0 +1,159 @@
<?php
use App\Models\Service;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
beforeEach(function () {
Storage::fake('public');
Queue::fake();
$this->user = User::factory()->create();
$this->actingAs($this->user);
$this->service = Service::factory()->create();
});
/*
|--------------------------------------------------------------------------
| Key-Visual Upload
|--------------------------------------------------------------------------
*/
test('key visual upload with service scope sets column only', function () {
$file = makeImageUpload('keyvisual.png', 800, 600);
$response = $this->post(route('services.key-visual.store', $this->service), [
'file' => $file,
'scope' => 'service',
]);
$response->assertRedirect();
$response->assertSessionHas('success');
$this->service->refresh();
expect($this->service->key_visual_filename)->not->toBeNull();
expect($this->service->key_visual_filename)->toStartWith('slides/');
expect($this->service->key_visual_filename)->toEndWith('.jpg');
expect(Storage::disk('public')->exists($this->service->key_visual_filename))->toBeTrue();
expect(Setting::get('current_key_visual'))->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Background Upload (default scope)
|--------------------------------------------------------------------------
*/
test('background upload with default scope sets column and global setting', function () {
$file = makeImageUpload('background.png', 1920, 1080);
$response = $this->post(route('services.background.store', $this->service), [
'file' => $file,
'scope' => 'default',
]);
$response->assertRedirect();
$response->assertSessionHas('success');
$this->service->refresh();
expect($this->service->background_filename)->not->toBeNull();
expect($this->service->background_filename)->toStartWith('slides/');
expect(Storage::disk('public')->exists($this->service->background_filename))->toBeTrue();
expect(Setting::get('current_background'))->toBe($this->service->background_filename);
});
/*
|--------------------------------------------------------------------------
| Validation
|--------------------------------------------------------------------------
*/
test('key visual upload rejects non-image file with german error', function () {
$file = UploadedFile::fake()->create('notes.txt', 10, 'text/plain');
$response = $this->postJson(route('services.key-visual.store', $this->service), [
'file' => $file,
'scope' => 'service',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['file']);
expect($response->json('errors.file.0'))->toContain('Bild');
$this->service->refresh();
expect($this->service->key_visual_filename)->toBeNull();
});
test('background upload rejects invalid scope', function () {
$file = makeImageUpload('background.png', 800, 600);
$response = $this->postJson(route('services.background.store', $this->service), [
'file' => $file,
'scope' => 'bogus',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['scope']);
});
test('key visual upload does not delete previous file', function () {
$first = makeImageUpload('first.png', 800, 600);
$this->post(route('services.key-visual.store', $this->service), [
'file' => $first,
'scope' => 'service',
]);
$this->service->refresh();
$oldFilename = $this->service->key_visual_filename;
$second = makeImageUpload('second.png', 800, 600);
$this->post(route('services.key-visual.store', $this->service), [
'file' => $second,
'scope' => 'service',
]);
$this->service->refresh();
expect($this->service->key_visual_filename)->not->toBe($oldFilename);
expect(Storage::disk('public')->exists($oldFilename))->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Auth
|--------------------------------------------------------------------------
*/
test('key visual upload requires authentication', function () {
auth()->logout();
$file = makeImageUpload('keyvisual.png', 800, 600);
$response = $this->post(route('services.key-visual.store', $this->service), [
'file' => $file,
'scope' => 'service',
]);
$response->assertRedirect(route('login'));
});
function makeImageUpload(string $name = 'test.png', int $w = 800, int $h = 600): UploadedFile
{
$path = tempnam(sys_get_temp_dir(), 'cts-svc-img-');
if ($path === false) {
throw new RuntimeException('Temp-Datei konnte nicht erstellt werden.');
}
$image = imagecreatetruecolor($w, $h);
if ($image === false) {
throw new RuntimeException('Bild konnte nicht erstellt werden.');
}
$blue = imagecolorallocate($image, 0, 0, 255);
imagefill($image, 0, 0, $blue);
imagepng($image, $path);
imagedestroy($image);
return new UploadedFile($path, $name, 'image/png', null, true);
}