removed all references to credits. now credits are completely replaced by addons.
This commit is contained in:
@@ -363,15 +363,7 @@ export type TenantPackageSummary = {
|
||||
|
||||
export type NotificationPreferences = Record<string, boolean>;
|
||||
|
||||
export type NotificationPreferencesMeta = {
|
||||
credit_warning_sent_at?: string | null;
|
||||
credit_warning_threshold?: number | null;
|
||||
};
|
||||
|
||||
export type CreditBalance = {
|
||||
balance: number;
|
||||
free_event_granted_at?: string | null;
|
||||
};
|
||||
export type NotificationPreferencesMeta = Record<string, never>;
|
||||
|
||||
export type PaddleTransactionSummary = {
|
||||
id: string | null;
|
||||
@@ -1975,37 +1967,6 @@ export async function getTenantAddonHistory(page = 1, perPage = 25): Promise<{
|
||||
return { data: rows, meta };
|
||||
}
|
||||
|
||||
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',
|
||||
@@ -2102,19 +2063,6 @@ export async function recordCreditPurchase(payload: {
|
||||
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 getTaskCollections(params: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface TenantProfile {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
email?: string | null;
|
||||
event_credits_balance?: number | null;
|
||||
features?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"generic": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
|
||||
"eventLimit": "Dein aktuelles Paket enthält keine freien Event-Slots mehr.",
|
||||
"eventLimitDetails": "{used} von {limit} Events genutzt. {remaining} verbleiben.",
|
||||
"creditsExhausted": "Keine Event-Slots mehr verfügbar. Bitte buche zusätzliche Slots oder upgrade dein Paket.",
|
||||
"photoLimit": "Für dieses Event ist das Foto-Upload-Limit erreicht.",
|
||||
"goToBilling": "Zur Paketverwaltung"
|
||||
},
|
||||
|
||||
@@ -1390,10 +1390,6 @@
|
||||
"packageExpired": {
|
||||
"label": "Paket ist abgelaufen",
|
||||
"description": "Benachrichtige mich, wenn das Paket abgelaufen ist."
|
||||
},
|
||||
"creditsLow": {
|
||||
"label": "Event-Slots werden knapp",
|
||||
"description": "Informiert mich bei niedrigen Slot-Schwellen."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"generic": "Something went wrong. Please try again.",
|
||||
"eventLimit": "Your current package has no remaining event slots.",
|
||||
"eventLimitDetails": "{used} of {limit} events used. {remaining} remaining.",
|
||||
"creditsExhausted": "You have no event slots remaining. Add more slots or upgrade your package.",
|
||||
"photoLimit": "This event reached its photo upload limit.",
|
||||
"goToBilling": "Manage subscription"
|
||||
},
|
||||
|
||||
@@ -1386,10 +1386,6 @@
|
||||
"packageExpired": {
|
||||
"label": "Package expired",
|
||||
"description": "Inform me once the package has expired."
|
||||
},
|
||||
"creditsLow": {
|
||||
"label": "Event slots running low",
|
||||
"description": "Warn me when slot thresholds are reached."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,11 +362,6 @@ export default function EventFormPage() {
|
||||
setShowUpgradeHint(true);
|
||||
break;
|
||||
}
|
||||
case 'event_credits_exhausted': {
|
||||
setError(tErrors('creditsExhausted'));
|
||||
setShowUpgradeHint(true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const metaErrors = Array.isArray(err.meta?.errors) ? err.meta.errors.filter(Boolean).join('\n') : null;
|
||||
setError(metaErrors || err.message || tErrors('generic'));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, CheckCircle2, Loader2, Lock, LogOut, Mail, Moon, ShieldCheck, SunMedium, UserCog } from 'lucide-react';
|
||||
import { AlertTriangle, Loader2, Lock, LogOut, Mail, Moon, ShieldCheck, SunMedium, UserCog } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
getNotificationPreferences,
|
||||
updateNotificationPreferences,
|
||||
NotificationPreferences,
|
||||
NotificationPreferencesMeta,
|
||||
} from '../api';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -40,7 +39,6 @@ export default function SettingsPage() {
|
||||
const [loadingNotifications, setLoadingNotifications] = React.useState(true);
|
||||
const [savingNotifications, setSavingNotifications] = React.useState(false);
|
||||
const [notificationError, setNotificationError] = React.useState<string | null>(null);
|
||||
const [notificationMeta, setNotificationMeta] = React.useState<NotificationPreferencesMeta | null>(null);
|
||||
|
||||
const heroDescription = t('settings.hero.description', { defaultValue: 'Gestalte das Erlebnis für dein Admin-Team – Darstellung, Benachrichtigungen und Session-Sicherheit.' });
|
||||
const heroSupporting = [
|
||||
@@ -222,7 +220,6 @@ export default function SettingsPage() {
|
||||
<NotificationPreferencesForm
|
||||
preferences={preferences}
|
||||
defaults={defaults}
|
||||
meta={notificationMeta}
|
||||
onChange={(next) => setPreferences(next)}
|
||||
onReset={() => setPreferences(defaults)}
|
||||
onSave={async () => {
|
||||
@@ -236,9 +233,6 @@ export default function SettingsPage() {
|
||||
if (updated.defaults && Object.keys(updated.defaults).length > 0) {
|
||||
setDefaults(updated.defaults);
|
||||
}
|
||||
if (updated.meta) {
|
||||
setNotificationMeta(updated.meta);
|
||||
}
|
||||
setNotificationError(null);
|
||||
} catch (error) {
|
||||
setNotificationError(
|
||||
@@ -255,7 +249,6 @@ export default function SettingsPage() {
|
||||
</SectionCard>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<NotificationMetaCard meta={notificationMeta} loading={loadingNotifications} translate={translateNotification} />
|
||||
<SupportCard />
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,7 +259,6 @@ export default function SettingsPage() {
|
||||
function NotificationPreferencesForm({
|
||||
preferences,
|
||||
defaults,
|
||||
meta,
|
||||
onChange,
|
||||
onReset,
|
||||
onSave,
|
||||
@@ -275,7 +267,6 @@ function NotificationPreferencesForm({
|
||||
}: {
|
||||
preferences: NotificationPreferences;
|
||||
defaults: NotificationPreferences;
|
||||
meta: NotificationPreferencesMeta | null;
|
||||
onChange: (next: NotificationPreferences) => void;
|
||||
onReset: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
@@ -283,22 +274,6 @@ function NotificationPreferencesForm({
|
||||
translate: (key: string, fallback?: string, options?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
const items = React.useMemo(() => buildPreferenceMeta(translate), [translate]);
|
||||
const locale = typeof window !== 'undefined' ? window.navigator.language : 'de-DE';
|
||||
const creditText = React.useMemo(() => {
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (meta.credit_warning_sent_at) {
|
||||
const date = formatDateTime(meta.credit_warning_sent_at, locale);
|
||||
|
||||
return translate('settings.notifications.meta.creditLast', 'Letzte Slot-Warnung: {{date}}', {
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
return translate('settings.notifications.meta.creditNever', 'Noch keine Slot-Warnung versendet.');
|
||||
}, [meta, translate, locale]);
|
||||
|
||||
return (
|
||||
<div className="relative space-y-4 pb-16">
|
||||
@@ -332,7 +307,6 @@ function NotificationPreferencesForm({
|
||||
{translate('settings.notifications.hint', 'Du kannst Benachrichtigungen jederzeit wieder aktivieren.')}
|
||||
</span>
|
||||
</div>
|
||||
{creditText ? <p className="text-xs text-slate-500 dark:text-slate-400">{creditText}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -391,11 +365,6 @@ function buildPreferenceMeta(
|
||||
label: translate('settings.notifications.items.packageExpired.label', 'Paket ist abgelaufen'),
|
||||
description: translate('settings.notifications.items.packageExpired.description', 'Benachrichtige mich, wenn das Paket abgelaufen ist.'),
|
||||
},
|
||||
{
|
||||
key: 'credits_low',
|
||||
label: translate('settings.notifications.items.creditsLow.label', 'Event-Slots werden knapp'),
|
||||
description: translate('settings.notifications.items.creditsLow.description', 'Informiert mich bei niedrigen Slot-Schwellen.'),
|
||||
},
|
||||
];
|
||||
|
||||
return map as Array<{ key: keyof NotificationPreferences; label: string; description: string }>;
|
||||
@@ -414,61 +383,6 @@ function NotificationSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationMetaCard({
|
||||
meta,
|
||||
loading,
|
||||
translate,
|
||||
}: {
|
||||
meta: NotificationPreferencesMeta | null;
|
||||
loading: boolean;
|
||||
translate: (key: string, fallback?: string, options?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
const locale = typeof window !== 'undefined' ? window.navigator.language : 'de-DE';
|
||||
const lastWarning = meta?.credit_warning_sent_at
|
||||
? formatDateTime(meta.credit_warning_sent_at, locale)
|
||||
: translate('settings.notifications.meta.creditNever', 'Noch keine Slot-Warnung versendet.');
|
||||
|
||||
return (
|
||||
<Card className="border border-slate-200 bg-white/90 shadow-sm dark:border-slate-800 dark:bg-slate-900/60">
|
||||
<CardContent className="space-y-4 p-5">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.3em] text-slate-500">{translate('settings.notifications.summary.badge', 'Status')}</p>
|
||||
<p className="mt-1 text-base font-semibold text-slate-900 dark:text-white">
|
||||
{translate('settings.notifications.summary.title', 'Benachrichtigungsübersicht')}
|
||||
</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
<div className="h-3 w-3/4 animate-pulse rounded bg-slate-200" />
|
||||
<div className="h-3 w-1/2 animate-pulse rounded bg-slate-100" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm text-slate-600 dark:text-slate-300">
|
||||
<div className="flex items-center gap-2 rounded-2xl border border-slate-200/80 bg-white/70 p-3 text-slate-800 dark:border-slate-700 dark:bg-slate-900/40 dark:text-white">
|
||||
<Mail className="h-4 w-4 text-primary" />
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-slate-300">{translate('settings.notifications.summary.channel', 'E-Mail Kanal')}</p>
|
||||
<p>{translate('settings.notifications.summary.channelCopy', 'Alle Warnungen werden per E-Mail versendet.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200/80 bg-amber-50 p-3 text-slate-800">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-amber-700">{translate('settings.notifications.summary.credits', 'Credits')}</p>
|
||||
<p>{lastWarning}</p>
|
||||
{meta?.credit_warning_threshold ? (
|
||||
<p className="text-xs text-amber-700/80">
|
||||
{translate('settings.notifications.summary.threshold', 'Warnung bei {{count}} verbleibenden Slots', {
|
||||
count: meta.credit_warning_threshold,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function SupportCard() {
|
||||
const { t } = useTranslation('management');
|
||||
return (
|
||||
@@ -492,22 +406,3 @@ function SupportCard() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(value: string, locale: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
} catch {
|
||||
return date.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user