diff --git a/src/components/aos/portal-shell.tsx b/src/components/aos/portal-shell.tsx index 1839098..4ae6c7a 100755 --- a/src/components/aos/portal-shell.tsx +++ b/src/components/aos/portal-shell.tsx @@ -37,10 +37,20 @@ import { AlertTriangle, Bell, Info, + CreditCard, + Plus, + Ban, } from 'lucide-react' +import { toast } from 'sonner' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { formatEuro, formatDate, formatRelative, daysUntil } from '@/lib/format' import { cn } from '@/lib/utils' -import { toast } from 'sonner' import { Popover, PopoverContent, @@ -94,6 +104,8 @@ interface PortalData { licenses: { id: string; key: string; product: string; plan: string; status: string validUntil: string | null; activations: { domain: string; environment: string }[] + netPriceCents: number; billingCycle: string; parentId: string | null + trialEndsAt: string | null; noticePeriodDays: number; cancelledAt: string | null }[] } @@ -503,6 +515,32 @@ function PortalApprovals({ data, refetch }: { data: PortalData; refetch: () => v } function PortalInvoices({ data }: { data: PortalData }) { + const [paying, setPaying] = useState(null) + const companyId = data.company.id + + const pay = async (licenseId: string) => { + setPaying(licenseId) + const t = toast.loading('Stripe Checkout wird vorbereitet…') + try { + const r = await fetch('/api/aos/stripe/checkout', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ licenseId }), + }) + const res = await r.json() + if (!r.ok) throw new Error(res.error || 'checkout failed') + toast.success('Weiterleitung zu Stripe…', { id: t }) + window.location.href = res.url + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Checkout fehlgeschlagen', { id: t }) + } finally { + setPaying(null) + } + } + + // Find licenses with a price > 0 that could be paid + const payableLicenses = data.licenses.filter((l) => l.netPriceCents > 0 && l.status === 'active' && !l.parentId) + return (
@@ -512,6 +550,46 @@ function PortalInvoices({ data }: { data: PortalData }) {

Deine Rechnungen — Ansicht nur für dein Unternehmen.

+ + {/* Pay subscription */} + {payableLicenses.length > 0 && ( + + +
+
+ +
+

Abonnement bezahlen

+

+ {payableLicenses.length} Lizenz(en) mit offener Zahlung. Klicke auf „Jetzt zahlen", um zur Stripe-Checkout-Seite zu gelangen. +

+
+
+
+
+ {payableLicenses.map((l) => ( +
+
+

{l.product} — {l.plan}

+

{formatEuro(l.netPriceCents)} / {l.billingCycle === 'yearly' ? 'Jahr' : 'Monat'}

+
+ +
+ ))} +
+
+
+ )} +
@@ -557,6 +635,47 @@ function PortalInvoices({ data }: { data: PortalData }) { } function PortalTickets({ data }: { data: PortalData }) { + const companyId = data.company.id + const [showCreate, setShowCreate] = useState(false) + const [subject, setSubject] = useState('') + const [category, setCategory] = useState('anfrage') + const [priority, setPriority] = useState<'low' | 'normal' | 'high' | 'urgent'>('normal') + const [message, setMessage] = useState('') + const [creating, setCreating] = useState(false) + + const createTicket = async () => { + if (!subject.trim() || !message.trim()) return + setCreating(true) + const t = toast.loading('Ticket wird erstellt…') + try { + const r = await fetch('/api/aos/tickets', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + companyId, + subject: subject.trim(), + categoryId: category, + priority, + message: message.trim(), + fromName: data.company.name, + }), + }) + const res = await r.json() + if (!r.ok) throw new Error(res.error || 'create failed') + toast.success(`Ticket ${res.ticket?.number ?? ''} erstellt`, { id: t }) + setShowCreate(false) + setSubject('') + setCategory('anfrage') + setPriority('normal') + setMessage('') + window.location.reload() + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Erstellung fehlgeschlagen', { id: t }) + } finally { + setCreating(false) + } + } + return (
@@ -567,7 +686,9 @@ function PortalTickets({ data }: { data: PortalData }) {

Support-Anfragen und Konversationen.

- +
{data.tickets.map((t) => ( @@ -587,11 +708,94 @@ function PortalTickets({ data }: { data: PortalData }) { ))} {data.tickets.length === 0 &&

Keine Tickets.

}
+ + {/* Create Ticket Dialog */} + + + + Neues Ticket + + Support-Anfrage erstellen. Dein Ticket wird an den zuständigen Reseller oder DFM Solutions weitergeleitet. + + +
+
+ + setSubject(e.target.value)} placeholder="Kurze Beschreibung des Problems" /> +
+
+
+ + +
+
+ + +
+
+
+ +