- Tenant-Admin-PWA: Neues /event-admin/welcome Onboarding mit WelcomeHero, Packages-, Order-Summary- und Event-Setup-Pages, Zustandsspeicher, Routing-Guard und Dashboard-CTA für Erstnutzer; Filament-/admin-Login via Custom-View behoben.
- Brand/Theming: Marketing-Farb- und Typographievariablen in `resources/css/app.css` eingeführt, AdminLayout, Dashboardkarten und Onboarding-Komponenten entsprechend angepasst; Dokumentation (`docs/todo/tenant-admin-onboarding-fusion.md`, `docs/changes/...`) aktualisiert. - Checkout & Payments: Checkout-, PayPal-Controller und Tests für integrierte Stripe/PayPal-Flows sowie Paket-Billing-Abläufe überarbeitet; neue PayPal SDK-Factory und Admin-API-Helper (`resources/js/admin/api.ts`) schaffen Grundlage für Billing/Members/Tasks-Seiten. - DX & Tests: Neue Playwright/E2E-Struktur (docs/testing/e2e.md, `tests/e2e/tenant-onboarding-flow.test.ts`, Utilities), E2E-Tenant-Seeder und zusätzliche Übersetzungen/Factories zur Unterstützung der neuen Flows. - Marketing-Kommunikation: Automatische Kontakt-Bestätigungsmail (`ContactConfirmation` + Blade-Template) implementiert; Guest-PWA unter `/event` erreichbar. - Nebensitzung: Blogsystem gefixt und umfassenden BlogPostSeeder für Beispielinhalte angelegt.
This commit is contained in:
@@ -38,6 +38,92 @@ export type EventStats = {
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type PaginationMeta = {
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type PaginatedResult<T> = {
|
||||
data: T[];
|
||||
meta: PaginationMeta;
|
||||
};
|
||||
|
||||
export type DashboardSummary = {
|
||||
active_events: number;
|
||||
new_photos: number;
|
||||
task_progress: number;
|
||||
credit_balance?: number | null;
|
||||
upcoming_events?: number | null;
|
||||
active_package?: {
|
||||
name: string;
|
||||
expires_at?: string | null;
|
||||
remaining_events?: number | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type TenantPackageSummary = {
|
||||
id: number;
|
||||
package_id: number;
|
||||
package_name: string;
|
||||
active: boolean;
|
||||
used_events: number;
|
||||
remaining_events: number | null;
|
||||
price: number | null;
|
||||
currency: string | null;
|
||||
purchased_at: string | null;
|
||||
expires_at: string | null;
|
||||
package_limits: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type CreditBalance = {
|
||||
balance: number;
|
||||
free_event_granted_at?: string | null;
|
||||
};
|
||||
|
||||
export type CreditLedgerEntry = {
|
||||
id: number;
|
||||
delta: number;
|
||||
reason: string;
|
||||
note: string | null;
|
||||
related_purchase_id: number | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type TenantTask = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent' | null;
|
||||
due_date: string | null;
|
||||
is_completed: boolean;
|
||||
collection_id: number | null;
|
||||
assigned_events_count: number;
|
||||
assigned_events?: TenantEvent[];
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
};
|
||||
|
||||
export type TaskPayload = Partial<{
|
||||
title: string;
|
||||
description: string | null;
|
||||
collection_id: number | null;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
due_date: string | null;
|
||||
is_completed: boolean;
|
||||
}>;
|
||||
|
||||
export type EventMember = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string | null;
|
||||
role: 'tenant_admin' | 'member' | 'guest' | string;
|
||||
status?: 'pending' | 'active' | 'invited' | string;
|
||||
joined_at?: string | null;
|
||||
avatar_url?: string | null;
|
||||
};
|
||||
|
||||
type EventListResponse = { data?: TenantEvent[] };
|
||||
type EventResponse = { data: TenantEvent };
|
||||
type CreatedEventResponse = { message: string; data: TenantEvent; balance: number };
|
||||
@@ -49,6 +135,7 @@ type EventSavePayload = {
|
||||
date?: string;
|
||||
status?: 'draft' | 'published' | 'archived';
|
||||
is_active?: boolean;
|
||||
package_id?: number;
|
||||
};
|
||||
|
||||
async function jsonOrThrow<T>(response: Response, message: string): Promise<T> {
|
||||
@@ -69,6 +156,16 @@ async function safeJson(response: Response): Promise<JsonValue | null> {
|
||||
}
|
||||
}
|
||||
|
||||
function buildPagination(payload: JsonValue | null, defaultCount: number): PaginationMeta {
|
||||
const meta = (payload?.meta as Partial<PaginationMeta>) ?? {};
|
||||
return {
|
||||
current_page: Number(meta.current_page ?? payload?.current_page ?? 1),
|
||||
last_page: Number(meta.last_page ?? payload?.last_page ?? 1),
|
||||
per_page: Number(meta.per_page ?? payload?.per_page ?? defaultCount ?? 0) || defaultCount || 0,
|
||||
total: Number(meta.total ?? payload?.total ?? defaultCount ?? 0) || defaultCount || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEvent(event: TenantEvent): TenantEvent {
|
||||
return {
|
||||
...event,
|
||||
@@ -93,6 +190,72 @@ function normalizePhoto(photo: TenantPhoto): TenantPhoto {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDashboard(payload: JsonValue | null): DashboardSummary | null {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
active_events: Number(payload.active_events ?? payload.activeEvents ?? 0),
|
||||
new_photos: Number(payload.new_photos ?? payload.newPhotos ?? 0),
|
||||
task_progress: Number(payload.task_progress ?? payload.taskProgress ?? 0),
|
||||
credit_balance: payload.credit_balance ?? payload.creditBalance ?? null,
|
||||
upcoming_events: payload.upcoming_events ?? payload.upcomingEvents ?? null,
|
||||
active_package: payload.active_package
|
||||
? {
|
||||
name: String(payload.active_package.name ?? 'Aktives Package'),
|
||||
expires_at: payload.active_package.expires_at ?? null,
|
||||
remaining_events: payload.active_package.remaining_events ?? payload.active_package.remainingEvents ?? null,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTenantPackage(pkg: JsonValue): TenantPackageSummary {
|
||||
const packageData = pkg.package ?? {};
|
||||
return {
|
||||
id: Number(pkg.id ?? 0),
|
||||
package_id: Number(pkg.package_id ?? packageData.id ?? 0),
|
||||
package_name: String(packageData.name ?? pkg.package_name ?? 'Unbekanntes Package'),
|
||||
active: Boolean(pkg.active ?? false),
|
||||
used_events: Number(pkg.used_events ?? 0),
|
||||
remaining_events: pkg.remaining_events !== undefined ? Number(pkg.remaining_events) : null,
|
||||
price: packageData.price !== undefined ? Number(packageData.price) : pkg.price ?? null,
|
||||
currency: packageData.currency ?? pkg.currency ?? 'EUR',
|
||||
purchased_at: pkg.purchased_at ?? pkg.created_at ?? null,
|
||||
expires_at: pkg.expires_at ?? pkg.valid_until ?? null,
|
||||
package_limits: pkg.package_limits ?? packageData.limits ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTask(task: JsonValue): TenantTask {
|
||||
return {
|
||||
id: Number(task.id ?? 0),
|
||||
title: String(task.title ?? 'Ohne Titel'),
|
||||
description: task.description ?? null,
|
||||
priority: (task.priority ?? null) as TenantTask['priority'],
|
||||
due_date: task.due_date ?? null,
|
||||
is_completed: Boolean(task.is_completed ?? false),
|
||||
collection_id: task.collection_id ?? null,
|
||||
assigned_events_count: Number(task.assigned_events_count ?? 0),
|
||||
assigned_events: Array.isArray(task.assigned_events) ? task.assigned_events.map(normalizeEvent) : undefined,
|
||||
created_at: task.created_at ?? null,
|
||||
updated_at: task.updated_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMember(member: JsonValue): EventMember {
|
||||
return {
|
||||
id: Number(member.id ?? 0),
|
||||
name: String(member.name ?? member.email ?? 'Unbekannt'),
|
||||
email: member.email ?? null,
|
||||
role: (member.role ?? 'member') as EventMember['role'],
|
||||
status: member.status ?? 'active',
|
||||
joined_at: member.joined_at ?? member.created_at ?? null,
|
||||
avatar_url: member.avatar_url ?? member.avatar ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function eventEndpoint(slug: string): string {
|
||||
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
|
||||
}
|
||||
@@ -194,3 +357,356 @@ export async function getPackages(type: 'endcustomer' | 'reseller' = 'endcustome
|
||||
const data = await jsonOrThrow<{ data: Package[] }>(response, 'Failed to load packages');
|
||||
return data.data ?? [];
|
||||
}
|
||||
|
||||
type TenantPackagesResponse = {
|
||||
data?: JsonValue[];
|
||||
active_package?: JsonValue | null;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type LedgerResponse = {
|
||||
data?: JsonValue[];
|
||||
meta?: Partial<PaginationMeta>;
|
||||
current_page?: number;
|
||||
last_page?: number;
|
||||
per_page?: number;
|
||||
total?: number;
|
||||
};
|
||||
|
||||
type TaskCollectionResponse = {
|
||||
data?: JsonValue[];
|
||||
meta?: Partial<PaginationMeta>;
|
||||
current_page?: number;
|
||||
last_page?: number;
|
||||
per_page?: number;
|
||||
total?: number;
|
||||
};
|
||||
|
||||
type MemberResponse = {
|
||||
data?: JsonValue[];
|
||||
meta?: Partial<PaginationMeta>;
|
||||
current_page?: number;
|
||||
last_page?: number;
|
||||
per_page?: number;
|
||||
total?: number;
|
||||
};
|
||||
|
||||
async function fetchTenantPackagesEndpoint(): Promise<Response> {
|
||||
const first = await authorizedFetch('/api/v1/tenant/tenant/packages');
|
||||
if (first.status === 404) {
|
||||
return authorizedFetch('/api/v1/tenant/packages');
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
export async function getDashboardSummary(): Promise<DashboardSummary | null> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/dashboard');
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to load dashboard', response.status, payload);
|
||||
throw new Error('Failed to load dashboard');
|
||||
}
|
||||
const json = (await response.json()) as JsonValue;
|
||||
return normalizeDashboard(json);
|
||||
}
|
||||
|
||||
export async function getTenantPackagesOverview(): Promise<{
|
||||
packages: TenantPackageSummary[];
|
||||
activePackage: TenantPackageSummary | null;
|
||||
}> {
|
||||
const response = await fetchTenantPackagesEndpoint();
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to load tenant packages', response.status, payload);
|
||||
throw new Error('Failed to load tenant packages');
|
||||
}
|
||||
const data = (await response.json()) as TenantPackagesResponse;
|
||||
const packages = Array.isArray(data.data) ? data.data.map(normalizeTenantPackage) : [];
|
||||
const activePackage = data.active_package ? normalizeTenantPackage(data.active_package) : null;
|
||||
return { packages, activePackage };
|
||||
}
|
||||
|
||||
export async function getCreditBalance(): Promise<CreditBalance> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/credits/balance');
|
||||
if (response.status === 404) {
|
||||
return { balance: 0 };
|
||||
}
|
||||
const data = await jsonOrThrow<CreditBalance>(response, 'Failed to load credit balance');
|
||||
return { balance: Number(data.balance ?? 0), free_event_granted_at: data.free_event_granted_at ?? null };
|
||||
}
|
||||
|
||||
export async function getCreditLedger(page = 1): Promise<PaginatedResult<CreditLedgerEntry>> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/credits/ledger?page=${page}`);
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to load credit ledger', response.status, payload);
|
||||
throw new Error('Failed to load credit ledger');
|
||||
}
|
||||
const json = (await response.json()) as LedgerResponse;
|
||||
const entries = Array.isArray(json.data) ? json.data.map((entry) => ({
|
||||
id: Number(entry.id ?? 0),
|
||||
delta: Number(entry.delta ?? 0),
|
||||
reason: String(entry.reason ?? 'unknown'),
|
||||
note: entry.note ?? null,
|
||||
related_purchase_id: entry.related_purchase_id ?? null,
|
||||
created_at: entry.created_at ?? '',
|
||||
})) : [];
|
||||
return {
|
||||
data: entries,
|
||||
meta: buildPagination(json as JsonValue, entries.length),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTenantPackagePaymentIntent(packageId: number): Promise<string> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/packages/payment-intent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ package_id: packageId }),
|
||||
});
|
||||
|
||||
const data = await jsonOrThrow<{ client_secret: string }>(
|
||||
response,
|
||||
'Failed to create package payment intent'
|
||||
);
|
||||
|
||||
if (!data.client_secret) {
|
||||
throw new Error('Missing client secret in response');
|
||||
}
|
||||
|
||||
return data.client_secret;
|
||||
}
|
||||
|
||||
export async function completeTenantPackagePurchase(params: {
|
||||
packageId: number;
|
||||
paymentMethodId?: string;
|
||||
paypalOrderId?: string;
|
||||
}): Promise<void> {
|
||||
const { packageId, paymentMethodId, paypalOrderId } = params;
|
||||
const payload: Record<string, unknown> = { package_id: packageId };
|
||||
|
||||
if (paymentMethodId) {
|
||||
payload.payment_method_id = paymentMethodId;
|
||||
}
|
||||
|
||||
if (paypalOrderId) {
|
||||
payload.paypal_order_id = paypalOrderId;
|
||||
}
|
||||
|
||||
const response = await authorizedFetch('/api/v1/tenant/packages/complete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
await jsonOrThrow(response, 'Failed to complete package purchase');
|
||||
}
|
||||
|
||||
export async function assignFreeTenantPackage(packageId: number): Promise<void> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/packages/free', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ package_id: packageId }),
|
||||
});
|
||||
|
||||
await jsonOrThrow(response, 'Failed to assign free package');
|
||||
}
|
||||
|
||||
export async function createTenantPayPalOrder(packageId: number): Promise<string> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/packages/paypal-create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ package_id: packageId }),
|
||||
});
|
||||
|
||||
const data = await jsonOrThrow<{ orderID: string }>(response, 'Failed to create PayPal order');
|
||||
if (!data.orderID) {
|
||||
throw new Error('Missing PayPal order ID');
|
||||
}
|
||||
|
||||
return data.orderID;
|
||||
}
|
||||
|
||||
export async function captureTenantPayPalOrder(orderId: string): Promise<void> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/packages/paypal-capture', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ order_id: orderId }),
|
||||
});
|
||||
|
||||
await jsonOrThrow(response, 'Failed to capture PayPal order');
|
||||
}
|
||||
|
||||
export async function recordCreditPurchase(payload: {
|
||||
package_id: string;
|
||||
credits_added: number;
|
||||
platform?: string;
|
||||
transaction_id?: string;
|
||||
subscription_active?: boolean;
|
||||
}): Promise<CreditBalance> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/credits/purchase', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await jsonOrThrow<{ message: string; balance: number; subscription_active?: boolean }>(
|
||||
response,
|
||||
'Failed to record credit purchase'
|
||||
);
|
||||
return { balance: Number(data.balance ?? 0) };
|
||||
}
|
||||
|
||||
export async function syncCreditBalance(payload: {
|
||||
balance: number;
|
||||
subscription_active?: boolean;
|
||||
last_sync?: string;
|
||||
}): Promise<{ balance: number; subscription_active: boolean; server_time: string }> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/credits/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return jsonOrThrow(response, 'Failed to sync credit balance');
|
||||
}
|
||||
|
||||
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));
|
||||
if (params.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params.search) searchParams.set('search', params.search);
|
||||
|
||||
const response = await authorizedFetch(`/api/v1/tenant/tasks?${searchParams.toString()}`);
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to load tasks', response.status, payload);
|
||||
throw new Error('Failed to load tasks');
|
||||
}
|
||||
const json = (await response.json()) as TaskCollectionResponse;
|
||||
const tasks = Array.isArray(json.data) ? json.data.map(normalizeTask) : [];
|
||||
return {
|
||||
data: tasks,
|
||||
meta: buildPagination(json as JsonValue, tasks.length),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTask(payload: TaskPayload): Promise<TenantTask> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to create task');
|
||||
return normalizeTask(data.data);
|
||||
}
|
||||
|
||||
export async function updateTask(taskId: number, payload: TaskPayload): Promise<TenantTask> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/tasks/${taskId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to update task');
|
||||
return normalizeTask(data.data);
|
||||
}
|
||||
|
||||
export async function deleteTask(taskId: number): Promise<void> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/tasks/${taskId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to delete task', response.status, payload);
|
||||
throw new Error('Failed to delete task');
|
||||
}
|
||||
}
|
||||
|
||||
export async function assignTasksToEvent(eventId: number, taskIds: number[]): Promise<void> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/tasks/bulk-assign-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 assign tasks', response.status, payload);
|
||||
throw new Error('Failed to assign tasks');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEventTasks(eventId: number, page = 1): Promise<PaginatedResult<TenantTask>> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/tasks/event/${eventId}?page=${page}`);
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to load event tasks', response.status, payload);
|
||||
throw new Error('Failed to load event tasks');
|
||||
}
|
||||
const json = (await response.json()) as TaskCollectionResponse;
|
||||
const tasks = Array.isArray(json.data) ? json.data.map(normalizeTask) : [];
|
||||
return {
|
||||
data: tasks,
|
||||
meta: buildPagination(json as JsonValue, tasks.length),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getEventMembers(eventIdentifier: number | string, page = 1): Promise<PaginatedResult<EventMember>> {
|
||||
const response = await authorizedFetch(
|
||||
`/api/v1/tenant/events/${encodeURIComponent(String(eventIdentifier))}/members?page=${page}`
|
||||
);
|
||||
if (response.status === 404) {
|
||||
return { data: [], meta: { current_page: 1, last_page: 1, per_page: 0, total: 0 } };
|
||||
}
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to load event members', response.status, payload);
|
||||
throw new Error('Failed to load event members');
|
||||
}
|
||||
const json = (await response.json()) as MemberResponse;
|
||||
const members = Array.isArray(json.data) ? json.data.map(normalizeMember) : [];
|
||||
return {
|
||||
data: members,
|
||||
meta: buildPagination(json as JsonValue, members.length),
|
||||
};
|
||||
}
|
||||
|
||||
export async function inviteEventMember(eventIdentifier: number | string, payload: { email: string; role?: string; name?: string }): Promise<EventMember> {
|
||||
const response = await authorizedFetch(
|
||||
`/api/v1/tenant/events/${encodeURIComponent(String(eventIdentifier))}/members`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}
|
||||
);
|
||||
if (response.status === 404) {
|
||||
throw new Error('Mitgliederverwaltung ist fuer dieses Event noch nicht verfuegbar.');
|
||||
}
|
||||
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to invite member');
|
||||
return normalizeMember(data.data);
|
||||
}
|
||||
|
||||
export async function removeEventMember(eventIdentifier: number | string, memberId: number): Promise<void> {
|
||||
const response = await authorizedFetch(
|
||||
`/api/v1/tenant/events/${encodeURIComponent(String(eventIdentifier))}/members/${memberId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
if (response.status === 404) {
|
||||
throw new Error('Mitglied konnte nicht gefunden werden.');
|
||||
}
|
||||
if (!response.ok) {
|
||||
const payload = await safeJson(response);
|
||||
console.error('[API] Failed to remove member', response.status, payload);
|
||||
throw new Error('Failed to remove member');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user