diff --git a/MANACORE-TODOS.md b/MANACORE-TODOS.md index f06cb9c3a..42d6d0bbe 100644 --- a/MANACORE-TODOS.md +++ b/MANACORE-TODOS.md @@ -86,10 +86,14 @@ - `services/mana-core-auth/src/stripe/` - Stripe-Module - `services/mana-core-auth/src/credits/credits.service.ts` - Purchase-Methoden +**Frontend (Implementiert 2026-02-13):** + +- [x] Credits-Seite: Stripe Checkout Integration +- [x] Loading-States und Toast-Benachrichtigungen + **Noch offen:** - [ ] Rechnungs-PDF generieren -- [ ] Frontend: Stripe Elements einbinden --- @@ -177,12 +181,12 @@ Archivierte Apps (memoro, storyteller) wurden bereits entfernt. - `services/mana-core-auth/src/auth/dto/change-password.dto.ts` - Passwort-Ändern DTO - `services/mana-core-auth/src/auth/dto/delete-account.dto.ts` - Konto-Löschen DTO -**Noch offen (Frontend):** +**Frontend (Implementiert 2026-02-13):** -- [ ] Profil-Edit Modal/Seite erstellen -- [ ] Passwort-Ändern Dialog -- [ ] Konto-Löschung mit Bestätigung -- [ ] Avatar-Upload mit S3/MinIO Integration +- [x] Profil-Edit Modal erstellt (`EditProfileModal.svelte`) +- [x] Passwort-Ändern Dialog erstellt (`ChangePasswordModal.svelte`) +- [x] Konto-Löschung mit Bestätigung (`DeleteAccountModal.svelte`) +- [ ] Avatar-Upload mit S3/MinIO Integration (noch offen) --- @@ -246,10 +250,13 @@ POST /api/v1/subscriptions/reactivate # Reaktivieren GET /api/v1/subscriptions/invoices # Rechnungen ``` -**Noch offen (Frontend):** +**Frontend (Implementiert 2026-02-13):** -- [ ] Plan-Übersicht Seite im Frontend -- [ ] Plan-Vergleichs-UI +- [x] Plan-Übersicht Seite im Frontend (`/subscription`) +- [x] Plan-Vergleichs-UI mit monatlich/jährlich Toggle +- [x] Stripe Checkout Integration für Subscriptions +- [x] Billing Portal Integration +- [x] Rechnungsübersicht - [ ] Stripe Price IDs in DB eintragen (nach Stripe-Setup) --- @@ -399,4 +406,4 @@ Diese Tasks können schnell erledigt werden: --- -_Zuletzt aktualisiert: 2026-02-13 (Profile-Features Backend)_ +_Zuletzt aktualisiert: 2026-02-13 (Profile + Subscription + Credits Frontend)_ diff --git a/apps/manacore/apps/web/src/lib/api/credits.ts b/apps/manacore/apps/web/src/lib/api/credits.ts index e24a11ab1..f69549952 100644 --- a/apps/manacore/apps/web/src/lib/api/credits.ts +++ b/apps/manacore/apps/web/src/lib/api/credits.ts @@ -119,4 +119,22 @@ export const creditsService = { body: JSON.stringify({ amount, appId, description }), }); }, + + /** + * Initiate a credit purchase via Stripe Checkout + */ + async initiatePurchase(packageId: string): Promise<{ + purchaseId: string; + checkoutUrl: string; + amount: number; + credits: number; + }> { + const successUrl = `${window.location.origin}/credits?success=true`; + const cancelUrl = `${window.location.origin}/credits?canceled=true`; + + return fetchWithAuth('/api/v1/credits/purchase', { + method: 'POST', + body: JSON.stringify({ packageId, successUrl, cancelUrl }), + }); + }, }; diff --git a/apps/manacore/apps/web/src/lib/api/profile.ts b/apps/manacore/apps/web/src/lib/api/profile.ts new file mode 100644 index 000000000..a23aa516d --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/profile.ts @@ -0,0 +1,99 @@ +/** + * Profile Service for ManaCore Web App + * Handles profile updates, password changes, and account deletion + */ + +import { authStore } from '$lib/stores/auth.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +// Types +export interface UserProfile { + id: string; + name: string; + email: string; + emailVerified: boolean; + image?: string; + role: string; + createdAt: string; +} + +export interface UpdateProfileRequest { + name?: string; + image?: string; +} + +export interface ChangePasswordRequest { + currentPassword: string; + newPassword: string; +} + +export interface DeleteAccountRequest { + password: string; + reason?: string; +} + +// Helper function for authenticated requests +async function fetchWithAuth(endpoint: string, options: RequestInit = {}): Promise { + const token = await authStore.getAccessToken(); + + const response = await fetch(`${MANA_AUTH_URL}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options.headers, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Request failed' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + + return response.json(); +} + +// Profile Service +export const profileService = { + /** + * Get current user profile + */ + async getProfile(): Promise { + return fetchWithAuth('/api/v1/auth/profile'); + }, + + /** + * Update user profile + */ + async updateProfile( + data: UpdateProfileRequest + ): Promise<{ success: boolean; user: UserProfile }> { + return fetchWithAuth('/api/v1/auth/profile', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + /** + * Change password + */ + async changePassword( + data: ChangePasswordRequest + ): Promise<{ success: boolean; message: string }> { + return fetchWithAuth('/api/v1/auth/change-password', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + /** + * Delete account (soft-delete) + */ + async deleteAccount(data: DeleteAccountRequest): Promise<{ success: boolean; message: string }> { + return fetchWithAuth('/api/v1/auth/account', { + method: 'DELETE', + body: JSON.stringify(data), + }); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/api/subscriptions.ts b/apps/manacore/apps/web/src/lib/api/subscriptions.ts new file mode 100644 index 000000000..848ea11ff --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/subscriptions.ts @@ -0,0 +1,157 @@ +/** + * Subscriptions Service for ManaCore Web App + * Handles subscription plans, checkout, and billing portal + */ + +import { authStore } from '$lib/stores/auth.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; + +// Types +export interface SubscriptionPlan { + id: string; + name: string; + description?: string; + monthlyCredits: number; + priceMonthlyEuroCents: number; + priceYearlyEuroCents: number; + features: string[]; + isDefault: boolean; + active: boolean; + sortOrder: number; + stripePriceIdMonthly?: string; + stripePriceIdYearly?: string; +} + +export interface Subscription { + id: string; + userId: string; + planId: string; + status: 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete'; + billingInterval: 'month' | 'year'; + currentPeriodStart: string; + currentPeriodEnd: string; + cancelAtPeriodEnd: boolean; + stripeSubscriptionId?: string; +} + +export interface Invoice { + id: string; + number?: string; + status: string; + amountDueEuroCents: number; + amountPaidEuroCents: number; + currency: string; + hostedInvoiceUrl?: string; + invoicePdfUrl?: string; + periodStart?: string; + periodEnd?: string; + paidAt?: string; + createdAt: string; +} + +export interface CurrentSubscription { + plan: SubscriptionPlan | null; + subscription: Subscription | null; + isFreePlan: boolean; +} + +// Helper function for authenticated requests +async function fetchWithAuth(endpoint: string, options: RequestInit = {}): Promise { + const token = await authStore.getAccessToken(); + + const response = await fetch(`${MANA_AUTH_URL}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options.headers, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Request failed' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + + return response.json(); +} + +// Subscriptions Service +export const subscriptionsService = { + /** + * Get all available plans (public) + */ + async getPlans(): Promise { + const response = await fetch(`${MANA_AUTH_URL}/api/v1/subscriptions/plans`); + if (!response.ok) { + throw new Error('Failed to fetch plans'); + } + return response.json(); + }, + + /** + * Get current subscription + */ + async getCurrentSubscription(): Promise { + return fetchWithAuth('/api/v1/subscriptions/current'); + }, + + /** + * Create checkout session for subscription + */ + async createCheckout( + planId: string, + billingInterval: 'month' | 'year' + ): Promise<{ sessionId: string; url: string }> { + const successUrl = `${window.location.origin}/subscription?success=true`; + const cancelUrl = `${window.location.origin}/subscription?canceled=true`; + + return fetchWithAuth('/api/v1/subscriptions/checkout', { + method: 'POST', + body: JSON.stringify({ + planId, + billingInterval, + successUrl, + cancelUrl, + }), + }); + }, + + /** + * Open billing portal for self-service + */ + async openPortal(): Promise<{ url: string }> { + const returnUrl = `${window.location.origin}/subscription`; + + return fetchWithAuth('/api/v1/subscriptions/portal', { + method: 'POST', + body: JSON.stringify({ returnUrl }), + }); + }, + + /** + * Cancel subscription (at period end) + */ + async cancelSubscription(): Promise<{ success: boolean; cancelAtPeriodEnd: boolean }> { + return fetchWithAuth('/api/v1/subscriptions/cancel', { + method: 'POST', + }); + }, + + /** + * Reactivate canceled subscription + */ + async reactivateSubscription(): Promise<{ success: boolean }> { + return fetchWithAuth('/api/v1/subscriptions/reactivate', { + method: 'POST', + }); + }, + + /** + * Get invoice history + */ + async getInvoices(limit = 20): Promise { + return fetchWithAuth(`/api/v1/subscriptions/invoices?limit=${limit}`); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/components/profile/ChangePasswordModal.svelte b/apps/manacore/apps/web/src/lib/components/profile/ChangePasswordModal.svelte new file mode 100644 index 000000000..000a131df --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/profile/ChangePasswordModal.svelte @@ -0,0 +1,227 @@ + + +{#if show} + +
+ +
+{/if} diff --git a/apps/manacore/apps/web/src/lib/components/profile/DeleteAccountModal.svelte b/apps/manacore/apps/web/src/lib/components/profile/DeleteAccountModal.svelte new file mode 100644 index 000000000..e6053a76b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/profile/DeleteAccountModal.svelte @@ -0,0 +1,260 @@ + + +{#if show} + +
+ +
+{/if} diff --git a/apps/manacore/apps/web/src/lib/components/profile/EditProfileModal.svelte b/apps/manacore/apps/web/src/lib/components/profile/EditProfileModal.svelte new file mode 100644 index 000000000..56408363b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/profile/EditProfileModal.svelte @@ -0,0 +1,150 @@ + + +{#if show} + +
+ +
+{/if} diff --git a/apps/manacore/apps/web/src/lib/components/profile/index.ts b/apps/manacore/apps/web/src/lib/components/profile/index.ts new file mode 100644 index 000000000..c416171aa --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/profile/index.ts @@ -0,0 +1,3 @@ +export { default as EditProfileModal } from './EditProfileModal.svelte'; +export { default as ChangePasswordModal } from './ChangePasswordModal.svelte'; +export { default as DeleteAccountModal } from './DeleteAccountModal.svelte'; diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/de.json b/apps/manacore/apps/web/src/lib/i18n/locales/de.json index 2502be6f5..cfb01c168 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/de.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/de.json @@ -114,6 +114,42 @@ "total_spent": "Verbraucht", "manage": "Credits verwalten" }, + "profile": { + "title": "Profil", + "edit": "Profil bearbeiten", + "change_password": "Passwort ändern", + "delete_account": "Konto löschen", + "logout": "Abmelden", + "name": "Name", + "email": "E-Mail", + "save": "Speichern", + "cancel": "Abbrechen", + "current_password": "Aktuelles Passwort", + "new_password": "Neues Passwort", + "confirm_password": "Passwort bestätigen", + "password_changed": "Passwort erfolgreich geändert", + "profile_updated": "Profil erfolgreich aktualisiert" + }, + "subscription": { + "title": "Abonnement", + "current_plan": "Aktueller Plan", + "free_plan": "Free Plan", + "upgrade": "Upgrade", + "cancel": "Kündigen", + "reactivate": "Reaktivieren", + "billing_portal": "Zahlungsmethode verwalten", + "monthly": "Monatlich", + "yearly": "Jährlich", + "save_percent": "Spare {percent}%", + "per_month": "/ Monat", + "per_year": "/ Jahr", + "mana_per_month": "Mana / Monat", + "features": "Features", + "select_plan": "Auswählen", + "current": "Aktuell", + "invoices": "Rechnungen", + "no_invoices": "Noch keine Rechnungen vorhanden" + }, "app_slider": { "title": "Teil des Mana Ökosystems", "memoro_desc": "KI-gestützte Sprachnotizen", diff --git a/apps/manacore/apps/web/src/lib/i18n/locales/en.json b/apps/manacore/apps/web/src/lib/i18n/locales/en.json index 4919f51ad..e0f416502 100644 --- a/apps/manacore/apps/web/src/lib/i18n/locales/en.json +++ b/apps/manacore/apps/web/src/lib/i18n/locales/en.json @@ -114,6 +114,42 @@ "total_spent": "Spent", "manage": "Manage credits" }, + "profile": { + "title": "Profile", + "edit": "Edit profile", + "change_password": "Change password", + "delete_account": "Delete account", + "logout": "Log out", + "name": "Name", + "email": "Email", + "save": "Save", + "cancel": "Cancel", + "current_password": "Current password", + "new_password": "New password", + "confirm_password": "Confirm password", + "password_changed": "Password changed successfully", + "profile_updated": "Profile updated successfully" + }, + "subscription": { + "title": "Subscription", + "current_plan": "Current plan", + "free_plan": "Free Plan", + "upgrade": "Upgrade", + "cancel": "Cancel", + "reactivate": "Reactivate", + "billing_portal": "Manage payment method", + "monthly": "Monthly", + "yearly": "Yearly", + "save_percent": "Save {percent}%", + "per_month": "/ month", + "per_year": "/ year", + "mana_per_month": "Mana / month", + "features": "Features", + "select_plan": "Select", + "current": "Current", + "invoices": "Invoices", + "no_invoices": "No invoices yet" + }, "app_slider": { "title": "Part of the Mana Ecosystem", "memoro_desc": "AI-powered voice notes", diff --git a/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte index b55c889ea..b2a6740fb 100644 --- a/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte @@ -1,5 +1,6 @@ @@ -230,7 +270,8 @@ {#each packages.slice(0, 3) as pkg} {/each} @@ -309,9 +357,18 @@

@@ -327,3 +384,30 @@ {/if} {/if} + + +{#if toastMessage} +
+ {toastMessage} +
+{/if} + + diff --git a/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte index e4aeb28d4..0c16d74fa 100644 --- a/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/profile/+page.svelte @@ -1,43 +1,152 @@ - +
+ +{:else} + +{/if} + + + (showEditModal = false)} + onSuccess={handleProfileUpdate} /> + + (showPasswordModal = false)} + onSuccess={handlePasswordChange} +/> + + (showDeleteModal = false)} + onSuccess={handleAccountDeleted} +/> + + +{#if toastMessage} +
+ {toastMessage} +
+{/if} + + diff --git a/apps/manacore/apps/web/src/routes/(app)/subscription/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/subscription/+page.svelte new file mode 100644 index 000000000..7d06f9cd6 --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/subscription/+page.svelte @@ -0,0 +1,505 @@ + + +
+ + + {#if loading} +
+
+
+ {:else if error} + +
+

{error}

+ +
+
+ {:else} + + {#if currentSubscription?.subscription} + {@const sub = currentSubscription.subscription} + {@const plan = currentSubscription.plan} + {@const status = getStatusBadge(sub.status)} + +
+
+
+

{plan?.name || 'Aktueller Plan'}

+ + {status.text} + +
+

+ {plan?.monthlyCredits.toLocaleString('de-DE')} Mana / Monat +

+
+
+ +
+
+ +
+
+

Abrechnungszeitraum

+

{sub.billingInterval === 'year' ? 'Jährlich' : 'Monatlich'}

+
+
+

Aktuelle Periode

+

+ {formatDate(sub.currentPeriodStart)} - {formatDate(sub.currentPeriodEnd)} +

+
+
+ {#if sub.cancelAtPeriodEnd} +

Endet am

+

{formatDate(sub.currentPeriodEnd)}

+ + {:else} +

Verlängert am

+

{formatDate(sub.currentPeriodEnd)}

+ + {/if} +
+
+
+ {:else} + + +
+
+
+

Free Plan

+ + Aktuell + +
+

+ 150 Mana / Monat +

+
+

+ Upgrade auf einen bezahlten Plan für mehr Mana und Features. +

+
+
+ {/if} + + +
+ + +
+ + + {#if activeTab === 'plans'} + +
+
+ + +
+
+ + +
+ {#each plans as plan} + {@const isCurrentPlan = currentSubscription?.plan?.id === plan.id} + {@const price = billingInterval === 'year' ? plan.priceYearlyEuroCents : plan.priceMonthlyEuroCents} + {@const savings = getSavingsPercent(plan.priceMonthlyEuroCents, plan.priceYearlyEuroCents)} + +
+ {#if isCurrentPlan} + + Dein Plan + + {/if} + +

{plan.name}

+ {#if plan.description} +

{plan.description}

+ {/if} + +
+ + {plan.isDefault ? 'Kostenlos' : formatPrice(price, billingInterval)} + + {#if !plan.isDefault} + + / {billingInterval === 'year' ? 'Jahr' : 'Monat'} + + {#if billingInterval === 'year' && savings > 0} +

+ {formatMonthlyEquivalent(plan.priceYearlyEuroCents)} / Monat +

+ {/if} + {/if} +
+ +

+ {plan.monthlyCredits.toLocaleString('de-DE')} Mana / Monat +

+ + {#if plan.features && plan.features.length > 0} +
    + {#each plan.features as feature} +
  • + + + + {feature} +
  • + {/each} +
+ {/if} + + +
+
+ {/each} +
+ {:else if activeTab === 'invoices'} + +

Rechnungsverlauf

+ {#if invoices.length === 0} +

Noch keine Rechnungen vorhanden.

+ {:else} +
+ + + + + + + + + + + + + {#each invoices as invoice} + + + + + + + + + {/each} + +
NummerDatumZeitraumBetragStatus
{invoice.number || '-'}{formatDate(invoice.createdAt)} + {#if invoice.periodStart && invoice.periodEnd} + {formatDate(invoice.periodStart)} - {formatDate(invoice.periodEnd)} + {:else} + - + {/if} + + {formatPrice(invoice.amountPaidEuroCents, 'month')} + + {#if invoice.status === 'paid'} + + Bezahlt + + {:else} + + {invoice.status} + + {/if} + + {#if invoice.invoicePdfUrl} + + PDF + + {/if} +
+
+ {/if} +
+ {/if} + {/if} +
+ + +{#if toastMessage} +
+ {toastMessage} +
+{/if} + +