removed all references to credits. now credits are completely replaced by addons.

This commit is contained in:
Codex Agent
2025-12-01 15:50:17 +01:00
parent b8e515a03c
commit 28539754a7
76 changed files with 97 additions and 2533 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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"
},

View File

@@ -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."
}
}
}

View File

@@ -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"
},

View File

@@ -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."
}
}
}

View File

@@ -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'));

View File

@@ -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();
}
}