diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts index 403ba89..3a99597 100644 --- a/src/api/billingApi.ts +++ b/src/api/billingApi.ts @@ -4,14 +4,12 @@ import { ApiRequestOptions } from '../hooks/useApi'; // TYPES & INTERFACES // ============================================================================ -export type BillingModel = 'PREPAY_MANDATE' | 'PREPAY_USER'; export type TransactionType = 'CREDIT' | 'DEBIT' | 'ADJUSTMENT'; export type ReferenceType = 'WORKFLOW' | 'PAYMENT' | 'ADMIN' | 'SYSTEM'; export interface BillingBalance { mandateId: string; mandateName: string; - billingModel: BillingModel; balance: number; currency: string; warningThreshold: number; @@ -41,16 +39,12 @@ export interface BillingTransaction { export interface BillingSettings { id: string; mandateId: string; - billingModel: BillingModel; - defaultUserCredit: number; warningThresholdPercent: number; notifyOnWarning: boolean; notifyEmails: string[]; } export interface BillingSettingsUpdate { - billingModel?: BillingModel; - defaultUserCredit?: number; warningThresholdPercent?: number; notifyOnWarning?: boolean; notifyEmails?: string[]; @@ -69,7 +63,6 @@ export interface AccountSummary { id: string; mandateId: string; userId?: string; - accountType: string; balance: number; warningThreshold: number; enabled: boolean; @@ -305,10 +298,8 @@ export async function fetchUsersForMandateAdmin( export interface MandateBalance { mandateId: string; mandateName: string; - billingModel: BillingModel; totalBalance: number; userCount: number; - defaultUserCredit: number; warningThresholdPercent: number; } diff --git a/src/api/storeApi.ts b/src/api/storeApi.ts index 6f728fd..deb8f1e 100644 --- a/src/api/storeApi.ts +++ b/src/api/storeApi.ts @@ -42,7 +42,6 @@ export interface UserMandate { id: string; name: string; label: string; - mandateType: string; } export interface SubscriptionInfo { diff --git a/src/components/Navigation/MandateNavigation.tsx b/src/components/Navigation/MandateNavigation.tsx index 9f9c1dc..616a942 100644 --- a/src/components/Navigation/MandateNavigation.tsx +++ b/src/components/Navigation/MandateNavigation.tsx @@ -33,6 +33,8 @@ import type { import { getPageIcon } from '../../config/pageRegistry'; import { FaSpinner, FaPen } from 'react-icons/fa'; import { TreeNavigation, type TreeItem, type TreeNodeItem } from './TreeNavigation'; +import { usePrompt } from '../../hooks/usePrompt'; +import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import styles from './MandateNavigation.module.css'; @@ -192,14 +194,19 @@ const EmptyState: React.FC = () => ( export const MandateNavigation: React.FC = () => { const { blocks, loading, refresh } = useNavigation('de'); + const { prompt, PromptDialog } = usePrompt(); + const { showWarning } = useToast(); - const _handleRename = useCallback((instanceId: string, currentLabel: string) => { - const newLabel = window.prompt('Neuer Name:', currentLabel); + const _handleRename = useCallback(async (instanceId: string, currentLabel: string) => { + const newLabel = await prompt('Neuer Name:', { title: 'Umbenennen', defaultValue: currentLabel }); if (!newLabel || newLabel.trim() === currentLabel) return; - api.patch(`/api/features/instances/${instanceId}/rename`, { label: newLabel.trim() }) - .then(() => refresh()) - .catch((err: any) => alert('Umbenennung fehlgeschlagen: ' + (err?.response?.data?.detail || err.message))); - }, [refresh]); + try { + await api.patch(`/api/features/instances/${instanceId}/rename`, { label: newLabel.trim() }); + refresh(); + } catch (err: any) { + showWarning('Fehler', 'Umbenennung fehlgeschlagen: ' + (err?.response?.data?.detail || err.message)); + } + }, [refresh, prompt, showWarning]); const navigationItems: TreeItem[] = useMemo(() => { const items: TreeItem[] = []; @@ -280,6 +287,7 @@ export const MandateNavigation: React.FC = () => { ) : ( )} + ); }; diff --git a/src/components/OnboardingWizard.tsx b/src/components/OnboardingWizard.tsx index 5815cbb..a1e9fa4 100644 --- a/src/components/OnboardingWizard.tsx +++ b/src/components/OnboardingWizard.tsx @@ -7,7 +7,7 @@ interface OnboardingWizardProps { } const OnboardingWizard: React.FC = ({ onComplete, onDismiss }) => { - const [mandateType, setMandateType] = useState<'personal' | 'company'>('personal'); + const [planKey, setPlanKey] = useState<'TRIAL_7D' | 'STANDARD_MONTHLY'>('TRIAL_7D'); const [companyName, setCompanyName] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -17,8 +17,8 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi setError(null); try { await api.post('/api/local/onboarding', { - mandateType, - companyName: mandateType === 'company' ? companyName : undefined, + planKey, + companyName: companyName.trim() || undefined, }); onComplete(); } catch (err: any) { @@ -40,58 +40,56 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi }}>

Willkommen bei PowerOn

- Wie möchtest du PowerOn nutzen? + Wähle dein Abo und leg los.

- {mandateType === 'company' && ( -
- - setCompanyName(e.target.value)} - placeholder="Name des Unternehmens" - style={{ - width: '100%', padding: '10px 12px', borderRadius: '6px', - border: '1px solid var(--border, #d1d5db)', fontSize: '1rem', - boxSizing: 'border-box', - }} - /> -
- )} +
+ + setCompanyName(e.target.value)} + placeholder="z. B. Firmenname oder Projektname" + style={{ + width: '100%', padding: '10px 12px', borderRadius: '6px', + border: '1px solid var(--border, #d1d5db)', fontSize: '1rem', + boxSizing: 'border-box', + }} + /> +
{error &&

{error}

} @@ -102,7 +100,7 @@ const OnboardingWizard: React.FC = ({ onComplete, onDismi }}> Später - + + + + + ); + }, [state, _handleConfirm, _handleCancel]); + + return { prompt, PromptDialog }; +} diff --git a/src/pages/admin/AdminMandatesPage.tsx b/src/pages/admin/AdminMandatesPage.tsx index f17c49f..cac150a 100644 --- a/src/pages/admin/AdminMandatesPage.tsx +++ b/src/pages/admin/AdminMandatesPage.tsx @@ -14,6 +14,7 @@ import { splitMandateAndBillingFromForm, } from '../../utils/mandateBillingFormMerge'; import { useToast } from '../../contexts/ToastContext'; +import { usePrompt } from '../../hooks/usePrompt'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; import { FaPlus, FaSync, FaBuilding, FaUsers, FaLock } from 'react-icons/fa'; @@ -23,6 +24,7 @@ export const AdminMandatesPage: React.FC = () => { const navigate = useNavigate(); const { request } = useApiRequest(); const { showWarning, showSuccess } = useToast(); + const { prompt, PromptDialog } = usePrompt(); const { mandates, columns, @@ -111,11 +113,18 @@ export const AdminMandatesPage: React.FC = () => { setEditingBillingWarning(null); }; - // Handle delete (confirmation handled by DeleteActionButton) - // System mandates (isSystem=true) are protected from deletion const handleDeleteMandate = async (mandate: Mandate) => { if (mandate.isSystem) { - return; // Safety guard - should not be reachable due to disabled button + return; + } + const entered = await prompt( + `Um den Mandanten "${mandate.name}" unwiderruflich zu löschen, geben Sie den Namen ein:`, + { title: 'Mandant löschen', confirmLabel: 'Löschen', variant: 'danger', placeholder: mandate.name }, + ); + if (entered === null) return; + if (entered !== mandate.name) { + showWarning('Löschung abgebrochen', 'Der eingegebene Name stimmt nicht überein.'); + return; } await handleDelete(mandate.id); }; @@ -267,6 +276,8 @@ export const AdminMandatesPage: React.FC = () => { )} + + {/* Edit Modal */} {editingFormData && (
= ({ settings, onSave, loading }) => { const [formData, setFormData] = useState({ - billingModel: (settings?.billingModel === 'PREPAY_USER' ? 'PREPAY_USER' : 'PREPAY_MANDATE') as BillingSettings['billingModel'], - defaultUserCredit: Number(settings?.defaultUserCredit ?? 0), warningThresholdPercent: Number(settings?.warningThresholdPercent ?? 10), notifyOnWarning: settings?.notifyOnWarning ?? true, }); @@ -96,8 +94,6 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi useEffect(() => { if (settings) { setFormData({ - billingModel: settings.billingModel === 'PREPAY_USER' ? 'PREPAY_USER' : 'PREPAY_MANDATE', - defaultUserCredit: Number(settings.defaultUserCredit ?? 0), warningThresholdPercent: Number(settings.warningThresholdPercent ?? 10), notifyOnWarning: settings.notifyOnWarning ?? true, }); @@ -130,32 +126,6 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi )}
-
-
- - -
- -
- - setFormData(prev => ({ ...prev, defaultUserCredit: Number(e.target.value) }))} - min="0" - step="0.01" - /> -
-
-
@@ -202,28 +172,15 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi // ============================================================================ interface CreditAdderProps { - settings: BillingSettings | null; - accounts: AccountSummary[]; - users: MandateUserSummary[]; onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise; } -const CreditAdder: React.FC = ({ settings, accounts, users, onAddCredit }) => { - const [selectedUserId, setSelectedUserId] = useState(''); +const CreditAdder: React.FC = ({ onAddCredit }) => { const [amount, setAmount] = useState(''); const [description, setDescription] = useState('Manuelle Buchung durch Admin'); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); - const isPrepayUser = settings?.billingModel === 'PREPAY_USER'; - - const accountsByUserId = accounts - .filter(acc => acc.accountType === 'USER') - .reduce((map, acc) => { - if (acc.userId) map[acc.userId] = acc; - return map; - }, {} as Record); - const _handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const numAmount = parseFloat(amount); @@ -236,7 +193,7 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on setMessage(null); try { - await onAddCredit(isPrepayUser ? selectedUserId : undefined, numAmount, description); + await onAddCredit(undefined, numAmount, description); const label = numAmount > 0 ? `${_formatCurrency(numAmount)} erfolgreich gutgeschrieben.` : `${_formatCurrency(Math.abs(numAmount))} erfolgreich abgezogen.`; @@ -260,31 +217,6 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on )} - {isPrepayUser && ( -
-
- - -
-
- )} -
@@ -313,7 +245,7 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on @@ -367,7 +299,7 @@ const AccountsOverview: React.FC = ({ accounts, users, lo
{accounts.map((account) => (
-

{account.accountType === 'MANDATE' ? 'Mandanten-Konto' : 'Benutzer-Konto'}

+

{!account.userId ? 'Mandanten-Konto' : 'Benutzer-Konto'}

{account.userId && User: {_userNameMap.get(account.userId) || account.userId}} Guthaben: {formatCurrency(account.balance)} @@ -782,9 +714,6 @@ export const BillingAdmin: React.FC = () => { <> {isSysAdmin && ( )} diff --git a/src/pages/billing/BillingDashboard.tsx b/src/pages/billing/BillingDashboard.tsx index b61b536..7525836 100644 --- a/src/pages/billing/BillingDashboard.tsx +++ b/src/pages/billing/BillingDashboard.tsx @@ -26,11 +26,6 @@ const BalanceCard: React.FC = ({ balance, onClick }) => { }).format(amount); }; - const getBillingModelLabel = (model: string) => { - if (model === 'PREPAY_USER') return 'Prepaid (Benutzer)'; - return 'Prepaid (Mandant)'; - }; - return (
= ({ balance, onClick }) => { >

{balance.mandateName}

- {getBillingModelLabel(balance.billingModel)}
{formatCurrency(balance.balance)} diff --git a/src/pages/billing/BillingDataView.tsx b/src/pages/billing/BillingDataView.tsx index feb44f1..5846177 100644 --- a/src/pages/billing/BillingDataView.tsx +++ b/src/pages/billing/BillingDataView.tsx @@ -13,14 +13,10 @@ import { FormGeneratorTable, ColumnConfig } from '../../components/FormGenerator import { FormGeneratorReport } from '../../components/FormGenerator/FormGeneratorReport'; import type { ReportSection, ReportFilterState, ReportChartDataPoint } from '../../components/FormGenerator/FormGeneratorReport'; import api from '../../api'; -import { useApiRequest } from '../../hooks/useApi'; import { useBilling, type BillingBalance } from '../../hooks/useBilling'; -import { createCheckoutSession, UserTransaction } from '../../api/billingApi'; -import { getUserDataCache } from '../../utils/userCache'; +import { UserTransaction } from '../../api/billingApi'; import styles from './Billing.module.css'; -const STRIPE_AMOUNT_PRESETS = [10, 25, 50, 100, 250, 500]; - // ============================================================================ // HELPER: Currency formatter // ============================================================================ @@ -52,28 +48,13 @@ interface ViewStatistics { interface BalanceCardProps { balance: BillingBalance; - onCheckout?: (mandateId: string, amount: number) => void; - checkoutLoading?: boolean; } -const BalanceCard: React.FC = ({ balance, onCheckout, checkoutLoading }) => { - const [selectedAmount, setSelectedAmount] = useState(STRIPE_AMOUNT_PRESETS[0]); - const [showCheckout, setShowCheckout] = useState(false); - - const _getBillingModelLabel = (model: string) => { - if (model === 'PREPAY_USER') return 'Prepaid (Benutzer)'; - return 'Prepaid (Mandant)'; - }; - - // Stripe top-up on this page: only personal prepaid wallets. Mandate pool (PREPAY_MANDATE) is topped up by mandate admins via Administration → Billing. - const canStripeTopUpHere = balance.billingModel === 'PREPAY_USER'; - const isMandatePrepaidPool = balance.billingModel === 'PREPAY_MANDATE'; - +const BalanceCard: React.FC = ({ balance }) => { return (

{balance.mandateName}

- {_getBillingModelLabel(balance.billingModel)}
{_formatCurrency(balance.balance)} @@ -83,60 +64,17 @@ const BalanceCard: React.FC = ({ balance, onCheckout, checkout Niedriges Guthaben
)} - {isMandatePrepaidPool && ( -

- Aufladung des Mandanten-Guthabens ist nur für Mandanten-Administratoren möglich (Menü Administration → Billing). -

- )} - {canStripeTopUpHere && onCheckout && ( -
- {!showCheckout ? ( - - ) : ( -
- - - -
- )} -
- )} +

+ Aufladung des Mandanten-Guthabens ist nur für Mandanten-Administratoren möglich (Menü Administration → Billing). +

); }; @@ -329,8 +267,6 @@ function _buildStatisticsSections(viewStats: ViewStatistics): ReportSection[] { export const BillingDataView: React.FC = () => { const [activeTab, setActiveTab] = useState('overview'); const [searchParams, setSearchParams] = useSearchParams(); - const { request } = useApiRequest(); - const [checkoutLoading, setCheckoutLoading] = useState(false); const [checkoutMessage, setCheckoutMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); // Scope filter: 'personal' | 'all' | mandateId @@ -399,31 +335,6 @@ export const BillingDataView: React.FC = () => { setCheckoutMessage(null); }, [searchParams, setSearchParams]); - const _handleCheckout = useCallback(async (mandateId: string, amount: number) => { - setCheckoutLoading(true); - setCheckoutMessage(null); - try { - const currentUser = getUserDataCache(); - const currentUrl = new URL(window.location.href); - currentUrl.searchParams.delete('success'); - currentUrl.searchParams.delete('canceled'); - currentUrl.searchParams.delete('session_id'); - currentUrl.hash = ''; - const returnUrl = `${currentUrl.origin}${currentUrl.pathname}${currentUrl.search}`; - const result = await createCheckoutSession(request, mandateId, { - userId: currentUser?.id, - amount, - returnUrl, - }); - if (result?.redirectUrl) { - window.location.href = result.redirectUrl; - } - } catch (err: any) { - setCheckoutMessage({ type: 'error', text: err.message || 'Fehler beim Erstellen der Checkout-Session' }); - setCheckoutLoading(false); - } - }, [request]); - // All user balances (for admin overview cards) const [allUserBalances, setAllUserBalances] = useState([]); const [allUserBalancesLoading, setAllUserBalancesLoading] = useState(false); @@ -666,8 +577,6 @@ export const BillingDataView: React.FC = () => { ))}
@@ -686,7 +595,7 @@ export const BillingDataView: React.FC = () => {

{ub.userName || ub.userId?.slice(0, 8)}

- {ub.mandateName} + {ub.mandateName}
{_formatCurrency(ub.balance || 0)} diff --git a/src/pages/billing/BillingMandateView.tsx b/src/pages/billing/BillingMandateView.tsx index 3279505..b85f149 100644 --- a/src/pages/billing/BillingMandateView.tsx +++ b/src/pages/billing/BillingMandateView.tsx @@ -38,20 +38,14 @@ const MandateBalanceTable: React.FC = ({ }).format(amount); }; - const getBillingModelLabel = (model: string) => { - if (model === 'PREPAY_USER') return 'Prepaid (Benutzer)'; - return 'Prepaid (Mandant)'; - }; - return (
- - + @@ -63,9 +57,8 @@ const MandateBalanceTable: React.FC = ({ className={selectedMandateId === balance.mandateId ? styles.selectedRow : ''} > - - +
MandantBilling-Modell Anzahl BenutzerStandard-GuthabenWarnschwelle (%) Gesamtguthaben Aktion
{balance.mandateName || balance.mandateId}{getBillingModelLabel(balance.billingModel)} {balance.userCount}{formatCurrency(balance.defaultUserCredit)}{balance.warningThresholdPercent}% {formatCurrency(balance.totalBalance)}