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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user