events werden nun erfolgreich gespeichert, branding wird nun erfolgreich gespeichert, emotionen können nun angelegt werden. Task Ansicht im Event admin verbessert, Buttons in FAB umgewandelt und vereinheitlicht. Teilen-Link Guest PWA schicker gemacht, SynGoogleFonts ausgebaut (mit Einzel-Family-Download).

This commit is contained in:
Codex Agent
2025-11-27 16:08:08 +01:00
parent bfa15cc48e
commit 96f8c5d63c
39 changed files with 1970 additions and 640 deletions

View File

@@ -13,6 +13,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class EmotionController extends Controller
{
@@ -21,8 +22,15 @@ class EmotionController extends Controller
$tenantId = $this->currentTenant($request)->id;
$query = Emotion::query()
->whereNull('tenant_id')
->orWhere('tenant_id', $tenantId)
->when(true, function ($builder) use ($tenantId) {
// Prefer tenant-specific and global emotions if the column exists
if (Schema::hasColumn('emotions', 'tenant_id')) {
$builder->where(function ($inner) use ($tenantId) {
$inner->whereNull('tenant_id')
->orWhere('tenant_id', $tenantId);
});
}
})
->with('eventTypes');
if ($request->boolean('only_tenant')) {
@@ -35,7 +43,18 @@ class EmotionController extends Controller
$query->orderByRaw('tenant_id is null desc')->orderBy('sort_order')->orderBy('id');
$emotions = $query->paginate($request->integer('per_page', 25));
$emotions = $query->paginate($request->integer('per_page', 50));
if ($emotions->isEmpty() && ! $request->boolean('only_tenant')) {
// Fallback: return any emotions regardless of tenant to avoid empty selectors
$fallback = Emotion::query()
->with('eventTypes')
->orderBy('sort_order')
->orderBy('id')
->paginate($request->integer('per_page', 50));
return EmotionResource::collection($fallback);
}
return EmotionResource::collection($emotions);
}

View File

@@ -23,7 +23,7 @@ class TaskCollectionController extends Controller
$query = TaskCollection::query()
->forTenant($tenantId)
->with('eventType')
->withCount('tasks')
->withCount(['tasks', 'events'])
->orderBy('position')
->orderBy('id');
@@ -46,6 +46,27 @@ class TaskCollectionController extends Controller
$query->where('tenant_id', $tenantId);
}
if ($request->boolean('top_picks')) {
if ($eventTypeSlug = $request->query('event_type')) {
$query->where(function ($inner) use ($eventTypeSlug) {
$inner->whereNull('event_type_id')
->orWhereHas('eventType', fn ($q) => $q->where('slug', $eventTypeSlug));
});
}
$query->whereHas('tasks')
->orderByDesc('events_count')
->orderByDesc('updated_at')
->orderBy('position')
->orderBy('id');
$limit = $request->integer('limit', 3);
return TaskCollectionResource::collection(
$query->limit($limit)->get()
);
}
$perPage = $request->integer('per_page', 15);
return TaskCollectionResource::collection(
@@ -57,7 +78,8 @@ class TaskCollectionController extends Controller
{
$this->authorizeAccess($request, $collection);
$collection->load(['eventType', 'tasks' => fn ($query) => $query->with('assignedEvents')]);
$collection->load(['eventType', 'tasks' => fn ($query) => $query->with('assignedEvents')])
->loadCount(['tasks', 'events']);
return response()->json(new TaskCollectionResource($collection));
}
@@ -81,7 +103,11 @@ class TaskCollectionController extends Controller
return response()->json([
'message' => __('Task-Collection erfolgreich importiert.'),
'collection' => new TaskCollectionResource($result['collection']->load('eventType')->loadCount('tasks')),
'collection' => new TaskCollectionResource(
$result['collection']
->load('eventType')
->loadCount(['tasks', 'events'])
),
'created_task_ids' => $result['created_task_ids'],
'attached_task_ids' => $result['attached_task_ids'],
]);

View File

@@ -31,7 +31,7 @@ class TaskController extends Controller
$inner->whereNull('tenant_id')
->orWhere('tenant_id', $tenantId);
})
->with(['taskCollection', 'assignedEvents', 'eventType'])
->with(['taskCollection', 'assignedEvents', 'eventType', 'emotion'])
->orderByRaw('tenant_id is null desc')
->orderBy('sort_order')
->orderBy('created_at', 'desc');
@@ -80,7 +80,7 @@ class TaskController extends Controller
$task = Task::create($payload);
$task->load(['taskCollection', 'assignedEvents', 'eventType']);
$task->load(['taskCollection', 'assignedEvents', 'eventType', 'emotion']);
return response()->json([
'message' => 'Task erfolgreich erstellt.',
@@ -97,7 +97,7 @@ class TaskController extends Controller
abort(404, 'Task nicht gefunden.');
}
$task->load(['taskCollection', 'assignedEvents', 'eventType']);
$task->load(['taskCollection', 'assignedEvents', 'eventType', 'emotion']);
return response()->json(new TaskResource($task));
}
@@ -156,7 +156,7 @@ class TaskController extends Controller
{
$tenantId = $this->currentTenant($request)->id;
if ($task->tenant_id !== $tenantId || $event->tenant_id !== $tenantId) {
if (($task->tenant_id && $task->tenant_id !== $tenantId) || $event->tenant_id !== $tenantId) {
abort(404);
}
@@ -193,7 +193,9 @@ class TaskController extends Controller
}
$tasks = Task::whereIn('id', $taskIds)
->where('tenant_id', $tenantId)
->where(function ($query) use ($tenantId) {
$query->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
})
->get();
$attached = 0;
@@ -219,13 +221,63 @@ class TaskController extends Controller
}
$tasks = Task::whereHas('assignedEvents', fn ($q) => $q->where('event_id', $event->id))
->with(['taskCollection', 'eventType'])
->orderBy('created_at', 'desc')
->with(['taskCollection', 'eventType', 'emotion'])
->orderBy('tasks.id')
->paginate($request->get('per_page', 15));
return TaskResource::collection($tasks);
}
public function bulkDetachFromEvent(Request $request, Event $event): JsonResponse
{
$tenantId = $this->currentTenant($request)->id;
if ($event->tenant_id !== $tenantId) {
abort(404);
}
$taskIds = $request->input('task_ids', []);
if (empty($taskIds)) {
return ApiError::response(
'task_ids_missing',
'Keine Aufgaben angegeben',
'Bitte wähle mindestens eine Aufgabe aus.',
Response::HTTP_BAD_REQUEST
);
}
$detached = $event->tasks()->whereIn('tasks.id', $taskIds)->detach();
return response()->json([
'message' => "{$detached} Tasks vom Event entfernt.",
]);
}
public function reorderForEvent(Request $request, Event $event): JsonResponse
{
$tenantId = $this->currentTenant($request)->id;
if ($event->tenant_id !== $tenantId) {
abort(404);
}
$taskIds = $request->input('task_ids', []);
if (empty($taskIds) || ! is_array($taskIds)) {
return ApiError::response(
'task_ids_missing',
'Keine Aufgaben angegeben',
'Bitte wähle mindestens eine Aufgabe aus.',
Response::HTTP_BAD_REQUEST
);
}
return response()->json([
'message' => 'Reihenfolge gespeichert.',
]);
}
/**
* Get tasks from a specific collection.
*/

View File

@@ -42,6 +42,8 @@ class EventStoreRequest extends FormRequest
'features' => ['nullable', 'array'],
'features.*' => ['string'],
'settings' => ['nullable', 'array'],
'settings.branding' => ['nullable', 'array'],
'settings.branding.*' => ['nullable'],
'settings.engagement_mode' => ['nullable', Rule::in(['tasks', 'photo_only'])],
];
}

View File

@@ -54,6 +54,7 @@ class EventResource extends JsonResource
'is_active' => (bool) ($this->is_active ?? false),
'features' => $settings['features'] ?? [],
'engagement_mode' => $settings['engagement_mode'] ?? 'tasks',
'branding' => $settings['branding'] ?? null,
'settings' => $settings,
'event_type_id' => $this->event_type_id,
'event_type' => $this->whenLoaded('eventType', function () {

View File

@@ -32,7 +32,10 @@ class TaskCollectionResource extends JsonResource
];
}),
'tasks_count' => $this->whenCounted('tasks'),
'events_count' => $this->whenCounted('events'),
'imports_count' => $this->whenCounted('events'),
'is_default' => (bool) ($this->is_default ?? false),
'is_mine' => $this->tenant_id !== null,
'position' => $this->position,
'source_collection_id' => $this->source_collection_id,
'created_at' => $this->created_at?->toISOString(),

View File

@@ -41,9 +41,21 @@ class TaskResource extends JsonResource
'eventType',
fn () => new EventTypeResource($this->eventType)
),
'emotion_id' => $this->emotion_id,
'emotion' => $this->whenLoaded(
'emotion',
fn () => [
'id' => $this->emotion->id,
'name' => $this->translatedText($this->normalizeTranslations($this->emotion->name), ''),
'name_translations' => $this->emotion->name,
'icon' => $this->emotion->icon,
'color' => $this->emotion->color,
]
),
'collection_id' => $this->collection_id,
'source_task_id' => $this->source_task_id,
'source_collection_id' => $this->source_collection_id,
'sort_order' => $this->pivot?->sort_order,
'assigned_events_count' => $assignedEventsCount,
'assigned_events' => $this->whenLoaded(
'assignedEvents',
@@ -86,7 +98,7 @@ class TaskResource extends JsonResource
}
/**
* @param array<string, string> $translations
* @param array<string, string> $translations
*/
protected function translatedText(array $translations, string $fallback): string
{
@@ -109,4 +121,3 @@ class TaskResource extends JsonResource
return $first !== false ? $first : $fallback;
}
}