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

@@ -433,10 +433,19 @@ export type TenantTask = {
is_completed: boolean;
event_type_id: number | null;
event_type?: TenantEventType | null;
emotion_id?: number | null;
emotion?: {
id: number;
name: string;
name_translations: Record<string, string>;
icon: string | null;
color: string | null;
} | null;
tenant_id: number | null;
collection_id: number | null;
source_task_id: number | null;
source_collection_id: number | null;
sort_order?: number | null;
assigned_events_count: number;
assigned_events?: TenantEvent[];
created_at: string | null;
@@ -452,6 +461,7 @@ export type TenantTaskCollection = {
description_translations: Record<string, string | null>;
tenant_id: number | null;
is_global: boolean;
is_mine?: boolean;
event_type?: {
id: number;
slug: string;
@@ -460,6 +470,8 @@ export type TenantTaskCollection = {
icon: string | null;
} | null;
tasks_count: number;
events_count?: number;
imports_count?: number;
position: number | null;
source_collection_id: number | null;
created_at: string | null;
@@ -951,6 +963,7 @@ function normalizeTask(task: JsonValue): TenantTask {
typeof task.event_type_id === 'number'
? Number(task.event_type_id)
: eventType?.id ?? null;
const emotionRaw = task.emotion ?? null;
return {
id: Number(task.id ?? 0),
@@ -969,6 +982,25 @@ function normalizeTask(task: JsonValue): TenantTask {
is_completed: Boolean(task.is_completed ?? false),
event_type_id: eventTypeId,
event_type: eventType,
sort_order:
typeof task.sort_order === 'number'
? Number(task.sort_order)
: task.pivot && typeof (task.pivot as { sort_order?: unknown }).sort_order === 'number'
? Number((task.pivot as { sort_order?: number }).sort_order)
: null,
emotion_id: typeof task.emotion_id === 'number' ? Number(task.emotion_id) : null,
emotion: emotionRaw
? {
id: Number(emotionRaw.id ?? 0),
name: pickTranslatedText(
normalizeTranslationMap(emotionRaw.name_translations ?? emotionRaw.name ?? {}),
String(emotionRaw.name ?? '')
),
name_translations: normalizeTranslationMap(emotionRaw.name_translations ?? emotionRaw.name ?? {}),
icon: emotionRaw.icon ?? null,
color: emotionRaw.color ?? null,
}
: null,
tenant_id: task.tenant_id ?? null,
collection_id: task.collection_id ?? null,
source_task_id: task.source_task_id ?? null,
@@ -1010,8 +1042,11 @@ function normalizeTaskCollection(raw: JsonValue): TenantTaskCollection {
description_translations: descriptionTranslations ?? {},
tenant_id: raw.tenant_id ?? null,
is_global: !raw.tenant_id,
is_mine: Boolean(raw.tenant_id),
event_type: eventType,
tasks_count: Number(raw.tasks_count ?? raw.tasksCount ?? 0),
events_count: raw.events_count !== undefined ? Number(raw.events_count) : undefined,
imports_count: raw.imports_count !== undefined ? Number(raw.imports_count) : undefined,
position: raw.position !== undefined ? Number(raw.position) : null,
source_collection_id: raw.source_collection_id ?? null,
created_at: raw.created_at ?? null,
@@ -1020,7 +1055,7 @@ function normalizeTaskCollection(raw: JsonValue): TenantTaskCollection {
}
function normalizeEmotion(raw: JsonValue): TenantEmotion {
const nameTranslations = normalizeTranslationMap(raw.name_translations ?? raw.name ?? {});
const nameTranslations = normalizeTranslationMap(raw.name_translations ?? raw.name ?? {}, undefined, true);
const descriptionTranslations = normalizeTranslationMap(
raw.description_translations ?? raw.description ?? {},
undefined,
@@ -1037,11 +1072,11 @@ function normalizeEmotion(raw: JsonValue): TenantEmotion {
name_translations: nameTranslations,
description: descriptionTranslations ? pickTranslatedText(descriptionTranslations, '') : null,
description_translations: descriptionTranslations ?? {},
icon: String(raw.icon ?? 'lucide-smile'),
icon: typeof raw.icon === 'string' ? raw.icon : 'lucide-smile',
color: String(raw.color ?? '#6366f1'),
sort_order: Number(raw.sort_order ?? 0),
is_active: Boolean(raw.is_active ?? true),
is_global: !raw.tenant_id,
is_global: raw.tenant_id === null || raw.tenant_id === undefined,
tenant_id: raw.tenant_id ?? null,
event_types: (eventTypes as JsonValue[]).map((eventType) => {
const translations = normalizeTranslationMap(eventType.name ?? {});
@@ -2086,6 +2121,8 @@ export async function getTaskCollections(params: {
search?: string;
event_type?: string;
scope?: 'global' | 'tenant';
top_picks?: boolean;
limit?: number;
} = {}): Promise<PaginatedResult<TenantTaskCollection>> {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set('page', String(params.page));
@@ -2093,6 +2130,8 @@ export async function getTaskCollections(params: {
if (params.search) searchParams.set('search', params.search);
if (params.event_type) searchParams.set('event_type', params.event_type);
if (params.scope) searchParams.set('scope', params.scope);
if (params.top_picks) searchParams.set('top_picks', '1');
if (params.limit) searchParams.set('limit', String(params.limit));
const queryString = searchParams.toString();
const response = await authorizedFetch(
@@ -2142,6 +2181,34 @@ export async function importTaskCollection(
throw new Error('Missing collection payload');
}
export async function detachTasksFromEvent(eventId: number, taskIds: number[]): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/bulk-detach-event/${eventId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_ids: taskIds }),
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to detach tasks', response.status, payload);
throw new Error('Failed to detach tasks');
}
}
export async function reorderEventTasks(eventId: number, taskIds: number[]): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/event/${eventId}/reorder`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_ids: taskIds }),
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to reorder tasks', response.status, payload);
throw new Error('Failed to reorder tasks');
}
}
export async function getEmotions(): Promise<TenantEmotion[]> {
const response = await authorizedFetch('/api/v1/tenant/emotions');
if (!response.ok) {
@@ -2176,6 +2243,17 @@ export async function updateEmotion(emotionId: number, payload: EmotionPayload):
return normalizeEmotion(json.data);
}
export async function deleteEmotion(emotionId: number): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/emotions/${emotionId}`, {
method: 'DELETE',
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to delete emotion', response.status, payload);
throw new Error('Failed to delete emotion');
}
}
export async function getTasks(params: { page?: number; per_page?: number; search?: string } = {}): Promise<PaginatedResult<TenantTask>> {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set('page', String(params.page));
@@ -2240,8 +2318,12 @@ export async function assignTasksToEvent(eventId: number, taskIds: number[]): Pr
}
}
export async function getEventTasks(eventId: number, page = 1): Promise<PaginatedResult<TenantTask>> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/event/${eventId}?page=${page}`);
export async function getEventTasks(
eventId: number,
page = 1,
perPage = 500,
): Promise<PaginatedResult<TenantTask>> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/event/${eventId}?page=${page}&per_page=${perPage}`);
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load event tasks', response.status, payload);