feat(macros): add 'abspann' macro assignment part type

Add new part type 'abspann' (credits/Abspann presentation) to the macro
assignment enum, validation and UI.

- Relax part_type from enum to plain string on macro_assignments,
  service_macro_overrides and service_macro_assignments so 'abspann' (and
  any controller-validated value) is accepted across MySQL and SQLite.
- Add 'abspann' to Rule::in validation in MacroAssignmentController and
  ServiceMacroOverrideController.
- Include 'abspann' in the per-service macros_per_part list in
  ServiceController so the override UI exposes it.
- Add 'abspann' to the parts array in Settings/MacroAssignments.vue
  (by_label stays song-only; abspann offers all/first/last slide).
This commit is contained in:
Thorsten Bus 2026-07-05 23:05:57 +02:00
parent 7a29a21822
commit e849d43b74
6 changed files with 90 additions and 6 deletions

View file

@ -35,7 +35,7 @@ public function index(): Response
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item,abspann'],
'macro_id' => ['required', 'integer', 'exists:macros,id'],
'position' => ['required', 'in:all_slides,first_slide,last_slide,by_label'],
'label_id' => ['nullable', 'integer', 'exists:labels,id'],
@ -50,7 +50,7 @@ public function store(Request $request): JsonResponse
public function update(Request $request, MacroAssignment $macroAssignment): JsonResponse
{
$validated = $request->validate([
'part_type' => ['sometimes', 'in:information,moderation,sermon,song,agenda_item'],
'part_type' => ['sometimes', 'in:information,moderation,sermon,song,agenda_item,abspann'],
'macro_id' => ['sometimes', 'integer', 'exists:macros,id'],
'position' => ['sometimes', 'in:all_slides,first_slide,last_slide,by_label'],
'label_id' => ['nullable', 'integer', 'exists:labels,id'],

View file

@ -247,7 +247,7 @@ public function edit(Service $service, \App\Services\ServiceImageResolver $image
// Macro resolution per part type (for icons + Anpassen/Standard panel)
$resolver = app(MacroResolutionService::class);
$macros_per_part = [];
foreach (['information', 'moderation', 'sermon', 'song', 'agenda_item'] as $partType) {
foreach (['information', 'moderation', 'sermon', 'song', 'agenda_item', 'abspann'] as $partType) {
$assignments = $resolver->resolveAssignmentsForPart($service, $partType);
$isOverridden = ServiceMacroOverride::where('service_id', $service->id)
->where('part_type', $partType)

View file

@ -14,7 +14,7 @@ class ServiceMacroOverrideController extends Controller
public function store(Request $request, Service $service): JsonResponse
{
$validated = $request->validate([
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item,abspann'],
]);
ServiceMacroOverride::firstOrCreate([
@ -40,7 +40,7 @@ public function store(Request $request, Service $service): JsonResponse
public function destroy(Service $service, Request $request): JsonResponse
{
$validated = $request->validate([
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item,abspann'],
]);
ServiceMacroOverride::where('service_id', $service->id)
@ -57,7 +57,7 @@ public function destroy(Service $service, Request $request): JsonResponse
public function storeAssignment(Request $request, Service $service): JsonResponse
{
$validated = $request->validate([
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item'],
'part_type' => ['required', 'in:information,moderation,sermon,song,agenda_item,abspann'],
'macro_id' => ['required', 'integer', 'exists:macros,id'],
'position' => ['required', 'in:all_slides,first_slide,last_slide,by_label'],
'label_id' => ['nullable', 'integer', 'exists:labels,id'],

View file

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Relax the `part_type` enum to a plain string so any controller-validated
* value (including the new 'abspann' part type) is accepted across MySQL and
* SQLite. The enum previously rejected 'abspann' via a MySQL enum definition
* or a SQLite check-constraint.
*/
public function up(): void
{
Schema::table('macro_assignments', function (Blueprint $table) {
$table->string('part_type')->change();
});
Schema::table('service_macro_overrides', function (Blueprint $table) {
$table->string('part_type')->change();
});
Schema::table('service_macro_assignments', function (Blueprint $table) {
$table->string('part_type')->change();
});
}
/**
* Best-effort restore of the original enum. Any 'abspann' rows would be lost
* on downgrade; a lossy down is acceptable for this local/dev project.
*/
public function down(): void
{
$original = ['information', 'moderation', 'sermon', 'song', 'agenda_item'];
Schema::table('macro_assignments', function (Blueprint $table) use ($original) {
$table->enum('part_type', $original)->change();
});
Schema::table('service_macro_overrides', function (Blueprint $table) use ($original) {
$table->enum('part_type', $original)->change();
});
Schema::table('service_macro_assignments', function (Blueprint $table) use ($original) {
$table->enum('part_type', $original)->change();
});
}
};

View file

@ -18,6 +18,7 @@ const parts = [
{ key: 'sermon', label: 'Predigt' },
{ key: 'song', label: 'Lieder' },
{ key: 'agenda_item', label: 'Agenda-Items' },
{ key: 'abspann', label: 'Abspann' },
]
const positions = [

View file

@ -30,6 +30,39 @@
expect(MacroAssignment::first()->part_type)->toBe('song');
});
test('store creates abspann macro assignment', function () {
$user = User::factory()->create();
$macro = Macro::factory()->create();
$response = $this->actingAs($user)
->postJson(route('settings.macro-assignments.store'), [
'part_type' => 'abspann',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$response->assertStatus(200)->assertJson(['success' => true]);
expect(MacroAssignment::where('part_type', 'abspann')->count())->toBe(1);
expect(MacroAssignment::first()->part_type)->toBe('abspann');
});
test('store rejects invalid part_type', function () {
$user = User::factory()->create();
$macro = Macro::factory()->create();
$response = $this->actingAs($user)
->postJson(route('settings.macro-assignments.store'), [
'part_type' => 'bogus',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$response->assertStatus(422);
expect(MacroAssignment::count())->toBe(0);
});
test('store with by_label position and label_id', function () {
$user = User::factory()->create();
$macro = Macro::factory()->create();