     1→     1→'use client'
     2→     2→
     3→     3→import { useEffect, useMemo, useState } from 'react'
     4→     4→import {
     5→     5→  Plus,
     6→     6→  KeyRound,
     7→     7→  ShieldCheck,
     8→     8→  AlertTriangle,
     9→     9→  CalendarClock,
    10→    10→  RefreshCw,
    11→    11→  CheckCircle2,
    12→    12→  XCircle,
    13→    13→  PauseCircle,
    14→    14→  ChevronRight,
    15→    15→  Ban,
    16→    16→  Play,
    17→    17→  RotateCcw,
    18→    18→  TrendingUp,
    19→    19→  FlaskConical,
    20→    20→  Loader2,
    21→    21→  CircleDollarSign,
    22→    22→} from 'lucide-react'
    23→    23→import { useFetch } from '@/hooks/use-fetch'
    24→    24→import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
    25→    25→import { Badge } from '@/components/ui/badge'
    26→    26→import { Button } from '@/components/ui/button'
    27→    27→import { Input } from '@/components/ui/input'
    28→    28→import { Label } from '@/components/ui/label'
    29→    29→import { Switch } from '@/components/ui/switch'
    30→    30→import { Textarea } from '@/components/ui/textarea'
    31→    31→import { Skeleton } from '@/components/ui/skeleton'
    32→    32→import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
    33→    33→import {
    34→    34→  Dialog,
    35→    35→  DialogContent,
    36→    36→  DialogDescription,
    37→    37→  DialogFooter,
    38→    38→  DialogHeader,
    39→    39→  DialogTitle,
    40→    40→} from '@/components/ui/dialog'
    41→    41→import {
    42→    42→  Select,
    43→    43→  SelectContent,
    44→    44→  SelectItem,
    45→    45→  SelectTrigger,
    46→    46→  SelectValue,
    47→    47→} from '@/components/ui/select'
    48→    48→import SlideCommit from '@/components/SlideCommit'
    49→    49→import { PageHeader } from '../primitives'
    50→    50→import { formatEuro, formatEuroCompact, formatDate, formatRelative, daysUntil } from '@/lib/format'
    51→    51→import { cn } from '@/lib/utils'
    52→    52→import { toast } from 'sonner'
    53→    53→
    54→    54→// ---------------------------------------------------------------------------
    55→    55→// Types
    56→    56→// ---------------------------------------------------------------------------
    57→    57→
    58→    58→interface LicenseModule {
    59→    59→  id: string
    60→    60→  key: string
    61→    61→  module: string | null
    62→    62→  status: string
    63→    63→  environment: 'demo' | 'production'
    64→    64→  maxActivations: number
    65→    65→  validUntil: string | null
    66→    66→  createdAt: string
    67→    67→  // B2B billing fields (mirror the parent License shape so module rows can
    68→    68→  // show their own billing state — most modules inherit "via Core" and have
    69→    69→  // priceCents = 0, but modules with their own pricing show their netPriceCents).
    70→    70→  billingCycle: 'monthly' | 'yearly'
    71→    71→  priceCents: number
    72→    72→  discountPct: number
    73→    73→  netPriceCents: number
    74→    74→  trialEndsAt: string | null
    75→    75→  subscriptionStartsAt: string | null
    76→    76→  cancelledAt: string | null
    77→    77→  cancellationEffectiveAt: string | null
    78→    78→}
    79→    79→
    80→    80→interface License {
    81→    81→  id: string
    82→    82→  key: string
    83→    83→  product: string
    84→    84→  plan: string
    85→    85→  status: string
    86→    86→  environment: 'demo' | 'production'
    87→    87→  parentId: string | null
    88→    88→  module: string | null
    89→    89→  maxActivations: number
    90→    90→  validUntil: string | null
    91→    91→  createdAt: string
    92→    92→  suspendReason: string | null
    93→    93→  suspendedAt: string | null
    94→    94→  // --- B2B Subscription & Billing (German law) ---
    95→    95→  noticePeriodDays: number
    96→    96→  trialEndsAt: string | null
    97→    97→  subscriptionStartsAt: string | null
    98→    98→  billingCycle: 'monthly' | 'yearly'
    99→    99→  priceCents: number
   100→   100→  discountPct: number
   101→   101→  netPriceCents: number
   102→   102→  cancelledAt: string | null
   103→   103→  cancellationEffectiveAt: string | null
   104→   104→  cancellationReason: string | null
   105→   105→  company: { id: string; name: string }
   106→   106→  activations: { id: string; domain: string; environment: string; version: string | null; lastCheck: string }[]
   107→   107→  modules: LicenseModule[]
   108→   108→}
   109→   109→
   110→   110→interface Asset {
   111→   111→  id: string
   112→   112→  name: string
   113→   113→  type: string
   114→   114→  provider: string | null
   115→   115→  costCentsPerMonth: number | null
   116→   116→  renewalDate: string
   117→   117→  rebill: boolean
   118→   118→  markupPct: number
   119→   119→  status: string
   120→   120→}
   121→   121→
   122→   122→interface LicensesData {
   123→   123→  licenses: License[]
   124→   124→  assets: Asset[]
   125→   125→  stats: {
   126→   126→    activeLicenses: number
   127→   127→    suspended: number
   128→   128→    expired: number
   129→   129→    cancelled: number
   130→   130→    inTrial: number
   131→   131→    totalActivations: number
   132→   132→    expiringSoon: number
   133→   133→    expiredAssets: number
   134→   134→    monthlyRebill: number
   135→   135→    demoLicenses: number
   136→   136→    mrrCents: number
   137→   137→  }
   138→   138→}
   139→   139→
   140→   140→// Minimal company shape for the create-license dialog. The CRM route returns
   141→   141→// the full Prisma Company (including the agency fields needed to preview the
   142→   142→// agency discount in the pricing breakdown).
   143→   143→interface CompanyLite {
   144→   144→  id: string
   145→   145→  name: string
   146→   146→  isAgency: boolean
   147→   147→  agencyStatus: string | null
   148→   148→  agencyDiscountPct: number
   149→   149→  parentAgencyId: string | null
   150→   150→}
   151→   151→
   152→   152→// ---------------------------------------------------------------------------
   153→   153→// Constants
   154→   154→// ---------------------------------------------------------------------------
   155→   155→
   156→   156→const LIC_STATUS: Record<string, { label: string; icon: typeof CheckCircle2 }> = {
   157→   157→  active: { label: 'Aktiv', icon: CheckCircle2 },
   158→   158→  suspended: { label: 'Suspendiert', icon: PauseCircle },
   159→   159→  expired: { label: 'Abgelaufen', icon: XCircle },
   160→   160→  revoked: { label: 'Widerrufen', icon: XCircle },
   161→   161→  cancelled: { label: 'Gekündigt', icon: Ban },
   162→   162→}
   163→   163→
   164→   164→const ASSET_TYPE_LABELS: Record<string, string> = {
   165→   165→  domain: 'Domain',
   166→   166→  hosting: 'Hosting',
   167→   167→  ssl: 'SSL',
   168→   168→  'plugin-seat': 'Plugin-Seat',
   169→   169→  saas: 'SaaS',
   170→   170→}
   171→   171→
   172→   172→// Default monthly prices per plan (mirror of the backend PLAN_PRICES table).
   173→   173→const PLAN_PRICES: Record<string, number> = {
   174→   174→  starter: 2900,
   175→   175→  pro: 5000,
   176→   176→  enterprise: 15000,
   177→   177→}
   178→   178→const YEARLY_DISCOUNT_PCT = 10
   179→   179→
   180→   180→type FilterKey = 'all' | 'active' | 'trial' | 'cancelled' | 'demo'
   181→   181→const FILTERS: { key: FilterKey; label: string }[] = [
   182→   182→  { key: 'all', label: 'Alle' },
   183→   183→  { key: 'active', label: 'Aktiv' },
   184→   184→  { key: 'trial', label: 'In Trial' },
   185→   185→  { key: 'cancelled', label: 'Gekündigt' },
   186→   186→  { key: 'demo', label: 'Demo' },
   187→   187→]
   188→   188→
   189→   189→// ---------------------------------------------------------------------------
   190→   190→// Main view
   191→   191→// ---------------------------------------------------------------------------
   192→   192→
   193→   193→export function LicensesView() {
   194→   194→  const { data, loading, refetch } = useFetch<LicensesData>('/api/aos/licenses')
   195→   195→  // Companies are needed for the create-license dialog (parent agency lookup).
   196→   196→  const { data: crmData } = useFetch<{ companies: CompanyLite[] } | null>('/api/aos/crm')
   197→   197→  const [createOpen, setCreateOpen] = useState(false)
   198→   198→  const [suspendTarget, setSuspendTarget] = useState<License | null>(null)
   199→   199→  const [cancelTarget, setCancelTarget] = useState<License | null>(null)
   200→   200→  const [filter, setFilter] = useState<FilterKey>('all')
   201→   201→
   202→   202→  if (loading || !data) {
   203→   203→    return (
   204→   204→      <div className="space-y-6 p-4 md:p-6">
   205→   205→        <Skeleton className="h-9 w-32" />
   206→   206→        <Skeleton className="h-48" />
   207→   207→      </div>
   208→   208→    )
   209→   209→  }
   210→   210→
   211→   211→  const { licenses, assets, stats } = data
   212→   212→  const companies = crmData?.companies ?? []
   213→   213→
   214→   214→  const now = new Date()
   215→   215→  const filtered = licenses.filter((l) => {
   216→   216→    if (filter === 'all') return true
   217→   217→    if (filter === 'demo') return l.environment === 'demo'
   218→   218→    if (filter === 'cancelled') return !!l.cancelledAt || l.status === 'cancelled'
   219→   219→    if (filter === 'trial') return !!l.trialEndsAt && new Date(l.trialEndsAt) > now
   220→   220→    if (filter === 'active') {
   221→   221→      return (
   222→   222→        l.status === 'active' &&
   223→   223→        !l.cancelledAt &&
   224→   224→        !(l.trialEndsAt && new Date(l.trialEndsAt) > now)
   225→   225→      )
   226→   226→    }
   227→   227→    return true
   228→   228→  })
   229→   229→
   230→   230→  const handlePromote = (license: License) => {
   231→   231→    void promoteLicense(license, refetch)
   232→   232→  }
   233→   233→
   234→   234→  return (
   235→   235→    <div className="space-y-6 p-4 md:p-6">
   236→   236→      <PageHeader
   237→   237→        title="Lizenzverwaltung"
   238→   238→        description="Ausgestellte Produktlizenzen mit Ed25519-signierten Tokens, B2B-Abrechnung (Monat/Jahr), 14-Tage-Testphase und 90-Tage-Kündigungsfrist. Erneuerungsregister für verwaltete Assets."
   239→   239→        actions={
   240→   240→          <Button size="sm" onClick={() => setCreateOpen(true)}>
   241→   241→            <Plus className="size-4" />
   242→   242→            Lizenz ausstellen
   243→   243→          </Button>
   244→   244→        }
   245→   245→      />
   246→   246→
   247→   247→      {/* KPI row — 4 existing + MRR + In Trial = 6 cards */}
   248→   248→      <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-6">
   249→   249→        <Card className="p-4">
   250→   250→          <div className="flex items-center justify-between">
   251→   251→            <p className="text-xs text-muted-foreground">Aktive Lizenzen</p>
   252→   252→            <KeyRound className="size-3.5 text-muted-foreground" />
   253→   253→          </div>
   254→   254→          <p className="mt-1 text-2xl font-semibold tabular-nums">{stats.activeLicenses}</p>
   255→   255→        </Card>
   256→   256→        <Card className="p-4">
   257→   257→          <div className="flex items-center justify-between">
   258→   258→            <p className="text-xs text-muted-foreground">Aktivierungen</p>
   259→   259→            <ShieldCheck className="size-3.5 text-muted-foreground" />
   260→   260→          </div>
   261→   261→          <p className="mt-1 text-2xl font-semibold tabular-nums">{stats.totalActivations}</p>
   262→   262→        </Card>
   263→   263→        <Card className="p-4">
   264→   264→          <div className="flex items-center justify-between">
   265→   265→            <p className="text-xs text-muted-foreground">Erneuerungen fällig</p>
   266→   266→            <AlertTriangle className="size-3.5 text-muted-foreground" />
   267→   267→          </div>
   268→   268→          <p className="mt-1 text-2xl font-semibold tabular-nums">{stats.expiringSoon + stats.expiredAssets}</p>
   269→   269→          <p className="text-[10px] text-muted-foreground">{stats.expiredAssets} abgelaufen</p>
   270→   270→        </Card>
   271→   271→        <Card className="p-4">
   272→   272→          <div className="flex items-center justify-between">
   273→   273→            <p className="text-xs text-muted-foreground">Monatl. Weiterbelastung</p>
   274→   274→            <CalendarClock className="size-3.5 text-muted-foreground" />
   275→   275→          </div>
   276→   276→          <p className="mt-1 text-2xl font-semibold tabular-nums">{formatEuro(stats.monthlyRebill)}</p>
   277→   277→        </Card>
   278→   278→        <Card className="p-4">
   279→   279→          <div className="flex items-center justify-between">
   280→   280→            <p className="text-xs text-muted-foreground">MRR</p>
   281→   281→            <TrendingUp className="size-3.5 text-muted-foreground" />
   282→   282→          </div>
   283→   283→          <p className="mt-1 text-2xl font-semibold tabular-nums">{formatEuroCompact(stats.mrrCents)}</p>
   284→   284→          <p className="text-[10px] text-muted-foreground">Monatlich wiederkehrend</p>
   285→   285→        </Card>
   286→   286→        <Card className="p-4">
   287→   287→          <div className="flex items-center justify-between">
   288→   288→            <p className="text-xs text-muted-foreground">In Trial</p>
   289→   289→            <FlaskConical className="size-3.5 text-muted-foreground" />
   290→   290→          </div>
   291→   291→          <p className="mt-1 text-2xl font-semibold tabular-nums">{stats.inTrial}</p>
   292→   292→          <p className="text-[10px] text-muted-foreground">14-Tage-Testphase</p>
   293→   293→        </Card>
   294→   294→      </div>
   295→   295→
   296→   296→      <Tabs defaultValue="licenses">
   297→   297→        <TabsList>
   298→   298→          <TabsTrigger value="licenses">Ausgestellte Lizenzen</TabsTrigger>
   299→   299→          <TabsTrigger value="assets">Erneuerungsregister</TabsTrigger>
   300→   300→        </TabsList>
   301→   301→
   302→   302→        <TabsContent value="licenses" className="mt-4">
   303→   303→          {/* Filter tabs */}
   304→   304→          <div className="mb-3 flex flex-wrap items-center gap-1.5">
   305→   305→            {FILTERS.map((f) => (
   306→   306→              <button
   307→   307→                key={f.key}
   308→   308→                onClick={() => setFilter(f.key)}
   309→   309→                className={cn(
   310→   310→                  'shrink-0 rounded-md border px-3 py-1.5 text-xs transition',
   311→   311→                  filter === f.key ? 'border-foreground/40 bg-foreground/5' : 'border-border hover:border-foreground/30'
   312→   312→                )}
   313→   313→              >
   314→   314→                {f.label}
   315→   315→              </button>
   316→   316→            ))}
   317→   317→          </div>
   318→   318→
   319→   319→          <Card>
   320→   320→            <CardHeader className="pb-2">
   321→   321→              <CardTitle className="text-sm">Produktlizenzen ({filtered.length})</CardTitle>
   322→   322→            </CardHeader>
   323→   323→            <CardContent className="p-0">
   324→   324→              <div className="overflow-x-auto">
   325→   325→                <table className="w-full text-sm">
   326→   326→                  <thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
   327→   327→                    <tr>
   328→   328→                      <th className="w-8 px-2 py-2" />
   329→   329→                      <th className="px-4 py-2 font-medium">Lizenzschlüssel</th>
   330→   330→                      <th className="px-4 py-2 font-medium">Produkt / Plan</th>
   331→   331→                      <th className="px-4 py-2 font-medium">Kunde</th>
   332→   332→                      <th className="px-4 py-2 font-medium">Aktivierungen</th>
   333→   333→                      <th className="px-4 py-2 font-medium">Status & Billing</th>
   334→   334→                      <th className="px-4 py-2 font-medium">Gültig bis</th>
   335→   335→                      <th className="px-4 py-2 font-medium">Aktionen</th>
   336→   336→                    </tr>
   337→   337→                  </thead>
   338→   338→                  <tbody className="divide-y divide-border">
   339→   339→                    {filtered.length === 0 ? (
   340→   340→                      <tr>
   341→   341→                        <td colSpan={8} className="px-4 py-10 text-center text-xs text-muted-foreground">
   342→   342→                          Keine Lizenzen in diesem Filter.
   343→   343→                        </td>
   344→   344→                      </tr>
   345→   345→                    ) : (
   346→   346→                      filtered.map((l) => (
   347→   347→                        <LicenseRow
   348→   348→                          key={l.id}
   349→   349→                          license={l}
   350→   350→                          onSuspend={(lic) => setSuspendTarget(lic)}
   351→   351→                          onCancel={(lic) => setCancelTarget(lic)}
   352→   352→                          onPromote={handlePromote}
   353→   353→                        />
   354→   354→                      ))
   355→   355→                    )}
   356→   356→                  </tbody>
   357→   357→                </table>
   358→   358→              </div>
   359→   359→            </CardContent>
   360→   360→          </Card>
   361→   361→
   362→   362→          {/* Signing info card */}
   363→   363→          <Card className="mt-4">
   364→   364→            <CardContent className="p-4">
   365→   365→              <div className="flex items-start gap-3">
   366→   366→                <div className="flex size-9 items-center justify-center rounded-md border border-border">
   367→   367→                  <ShieldCheck className="size-4 text-muted-foreground" />
   368→   368→                </div>
   369→   369→                <div className="flex-1">
   370→   370→                  <p className="text-sm font-medium">Ed25519-signierte Lizenz-Tokens</p>
   371→   371→                  <p className="mt-1 text-xs text-muted-foreground">
   372→   372→                    Jede Lizenz hat einen menschenlesbaren Schlüssel plus einen kryptografisch signierten Token. Kunden können offline verifizieren.
   373→   373→                    Der private Schlüssel ist verschlüsselt (AES-256-GCM); Rotation wird unterstützt.
   374→   374→                  </p>
   375→   375→                  <div className="mt-2 flex flex-wrap gap-2">
   376→   376→                    <Badge variant="outline" className="text-[10px]">Public API: /api/v1/validate</Badge>
   377→   377→                    <Badge variant="outline" className="text-[10px]">WordPress-Updater kompatibel</Badge>
   378→   378→                    <Badge variant="outline" className="text-[10px]">Offline-Grace: 14 d</Badge>
   379→   379→                  </div>
   380→   380→                </div>
   381→   381→              </div>
   382→   382→            </CardContent>
   383→   383→          </Card>
   384→   384→        </TabsContent>
   385→   385→
   386→   386→        <TabsContent value="assets" className="mt-4">
   387→   387→          <Card>
   388→   388→            <CardHeader className="pb-2">
   389→   389→              <CardTitle className="text-sm">Asset- & Erneuerungsregister ({assets.length})</CardTitle>
   390→   390→            </CardHeader>
   391→   391→            <CardContent className="p-0">
   392→   392→              <div className="overflow-x-auto">
   393→   393→                <table className="w-full text-sm">
   394→   394→                  <thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
   395→   395→                    <tr>
   396→   396→                      <th className="px-4 py-2 font-medium">Asset</th>
   397→   397→                      <th className="px-4 py-2 font-medium">Typ</th>
   398→   398→                      <th className="px-4 py-2 font-medium">Anbieter</th>
   399→   399→                      <th className="px-4 py-2 font-medium">Kosten/Monat</th>
   400→   400→                      <th className="px-4 py-2 font-medium">Erneuerung</th>
   401→   401→                      <th className="px-4 py-2 font-medium">Weiterbelasten</th>
   402→   402→                      <th className="px-4 py-2 font-medium">Status</th>
   403→   403→                    </tr>
   404→   404→                  </thead>
   405→   405→                  <tbody className="divide-y divide-border">
   406→   406→                    {assets.map((a) => {
   407→   407→                      const days = daysUntil(a.renewalDate)
   408→   408→                      return (
   409→   409→                        <tr key={a.id} className="transition hover:bg-muted/40">
   410→   410→                          <td className="px-4 py-2.5 text-xs font-medium">{a.name}</td>
   411→   411→                          <td className="px-4 py-2.5">
   412→   412→                            <Badge variant="secondary" className="text-[10px]">{ASSET_TYPE_LABELS[a.type] ?? a.type}</Badge>
   413→   413→                          </td>
   414→   414→                          <td className="px-4 py-2.5 text-xs text-muted-foreground">{a.provider ?? '—'}</td>
   415→   415→                          <td className="px-4 py-2.5 text-xs tabular-nums">
   416→   416→                            {a.costCentsPerMonth ? formatEuro(a.costCentsPerMonth) : '—'}
   417→   417→                          </td>
   418→   418→                          <td className="px-4 py-2.5 text-xs">
   419→   419→                            <span className={cn(days < 0 && 'text-foreground')}>
   420→   420→                              {formatDate(a.renewalDate)}
   421→   421→                            </span>
   422→   422→                            <span className="ml-1 text-[10px] text-muted-foreground">
   423→   423→                              {days < 0 ? `(${Math.abs(days)} d vorbei)` : `(${days} d)`}
   424→   424→                            </span>
   425→   425→                          </td>
   426→   426→                          <td className="px-4 py-2.5">
   427→   427→                            {a.rebill ? (
   428→   428→                              <Badge variant="outline" className="text-[10px] gap-1">
   429→   429→                                <RefreshCw className="size-2.5" />+{a.markupPct}%
   430→   430→                              </Badge>
   431→   431→                            ) : (
   432→   432→                              <span className="text-[10px] text-muted-foreground">—</span>
   433→   433→                            )}
   434→   434→                          </td>
   435→   435→                          <td className="px-4 py-2.5">
   436→   436→                            <Badge
   437→   437→                              variant="outline"
   438→   438→                              className={cn(
   439→   439→                                'text-[10px]',
   440→   440→                                a.status === 'expired' && 'border-foreground/40',
   441→   441→                                a.status === 'expiring' && 'border-foreground/30'
   442→   442→                              )}
   443→   443→                            >
   444→   444→                              {a.status === 'expired' ? 'Abgelaufen' : a.status === 'expiring' ? 'Läuft ab' : 'Aktiv'}
   445→   445→                            </Badge>
   446→   446→                          </td>
   447→   447→                        </tr>
   448→   448→                      )
   449→   449→                    })}
   450→   450→                  </tbody>
   451→   451→                </table>
   452→   452→              </div>
   453→   453→            </CardContent>
   454→   454→          </Card>
   455→   455→        </TabsContent>
   456→   456→      </Tabs>
   457→   457→
   458→   458→      <CreateLicenseDialog
   459→   459→        open={createOpen}
   460→   460→        onOpenChange={setCreateOpen}
   461→   461→        companies={companies}
   462→   462→        onCreated={refetch}
   463→   463→      />
   464→   464→      <SuspendDialog
   465→   465→        license={suspendTarget}
   466→   466→        onOpenChange={(o) => !o && setSuspendTarget(null)}
   467→   467→        onDone={refetch}
   468→   468→      />
   469→   469→      <CancelDialog
   470→   470→        license={cancelTarget}
   471→   471→        onOpenChange={(o) => !o && setCancelTarget(null)}
   472→   472→        onDone={refetch}
   473→   473→      />
   474→   474→    </div>
   475→   475→  )
   476→   476→}
   477→   477→
   478→   478→// ---------------------------------------------------------------------------
   479→   479→// LicenseRow — expandable row with billing badges + Cancel/Suspend/Promote
   480→   480→// ---------------------------------------------------------------------------
   481→   481→
   482→   482→function LicenseRow({
   483→   483→  license,
   484→   484→  onSuspend,
   485→   485→  onCancel,
   486→   486→  onPromote,
   487→   487→}: {
   488→   488→  license: License
   489→   489→  onSuspend: (l: License) => void
   490→   490→  onCancel: (l: License) => void
   491→   491→  onPromote: (l: License) => void
   492→   492→}) {
   493→   493→  const [open, setOpen] = useState(false)
   494→   494→  const st = LIC_STATUS[license.status] ?? LIC_STATUS.active
   495→   495→  const Icon = st.icon
   496→   496→  const valid = license.validUntil ? daysUntil(license.validUntil) : null
   497→   497→  const isModule = !!license.parentId
   498→   498→  const inTrial = !!license.trialEndsAt && new Date(license.trialEndsAt) > new Date()
   499→   499→  const trialDaysLeft = inTrial ? Math.max(0, daysUntil(license.trialEndsAt!)) : 0
   500→   500→  const cancelled = !!license.cancelledAt || license.status === 'cancelled'
   501→   501→  const canCancel = !isModule && !cancelled && license.status === 'active'
   502→   502→  const canReactivate = cancelled
   503→   503→  const hasModules = !isModule && license.modules.length > 0
   504→   504→  const showPrice = !isModule && license.netPriceCents > 0
   505→   505→
   506→   506→  return (
   507→   507→    <>
   508→   508→      <tr className="transition hover:bg-muted/40">
   509→   509→        <td className="px-2 py-2.5 align-middle">
   510→   510→          {hasModules ? (
   511→   511→            <button
   512→   512→              onClick={() => setOpen((o) => !o)}
   513→   513→              className="flex size-6 items-center justify-center rounded text-muted-foreground hover:bg-muted"
   514→   514→              aria-label={open ? 'Einklappen' : 'Ausklappen'}
   515→   515→            >
   516→   516→              <ChevronRight className={cn('size-4 transition-transform', open && 'rotate-90')} />
   517→   517→            </button>
   518→   518→          ) : (
   519→   519→            <span className="block size-6" />
   520→   520→          )}
   521→   521→        </td>
   522→   522→        <td className="px-4 py-2.5">
   523→   523→          <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px]">{license.key}</code>
   524→   524→          {isModule && (
   525→   525→            <Badge variant="outline" className="ml-1 text-[9px]">Modul</Badge>
   526→   526→          )}
   527→   527→        </td>
   528→   528→        <td className="px-4 py-2.5">
   529→   529→          <p className="text-xs font-medium">{license.product}</p>
   530→   530→          <Badge variant="outline" className="mt-0.5 text-[10px] capitalize">{license.plan}</Badge>
   531→   531→        </td>
   532→   532→        <td className="px-4 py-2.5 text-xs">{license.company.name}</td>
   533→   533→        <td className="px-4 py-2.5">
   534→   534→          <span className="text-xs tabular-nums">
   535→   535→            {license.activations.length} / {license.maxActivations}
   536→   536→          </span>
   537→   537→          {license.activations[0] && (
   538→   538→            <p className="text-[10px] text-muted-foreground">{license.activations[0].domain}</p>
   539→   539→          )}
   540→   540→        </td>
   541→   541→        <td className="px-4 py-2.5">
   542→   542→          <div className="flex flex-wrap items-center gap-1">
   543→   543→            <Badge
   544→   544→              variant="outline"
   545→   545→              className={cn(
   546→   546→                'gap-1 text-[10px]',
   547→   547→                cancelled ? 'border-foreground/40'
   548→   548→                  : inTrial ? 'border-foreground/30'
   549→   549→                  : license.status === 'active' ? 'border-foreground/40' : ''
   550→   550→              )}
   551→   551→            >
   552→   552→              <Icon className="size-3" />{st.label}
   553→   553→            </Badge>
   554→   554→            {showPrice && (
   555→   555→              <>
   556→   556→                <Badge
   557→   557→                  variant="outline"
   558→   558→                  className="text-[9px] tabular-nums"
   559→   559→                  title={license.billingCycle === 'yearly' ? 'Jährliche Abrechnung' : 'Monatliche Abrechnung'}
   560→   560→                >
   561→   561→                  {license.billingCycle === 'yearly' ? 'Y' : 'M'}
   562→   562→                </Badge>
   563→   563→                <span className="text-[11px] tabular-nums text-muted-foreground">
   564→   564→                  {formatEuroCompact(license.netPriceCents)}
   565→   565→                  <span className="text-[9px]">/{license.billingCycle === 'yearly' ? 'Jahr' : 'Mon.'}</span>
   566→   566→                </span>
   567→   567→              </>
   568→   568→            )}
   569→   569→            {inTrial && (
   570→   570→              <Badge variant="outline" className="gap-1 text-[9px] border-foreground/30">
   571→   571→                <FlaskConical className="size-2.5" />Trial ({trialDaysLeft} d)
   572→   572→              </Badge>
   573→   573→            )}
   574→   574→            {cancelled && license.cancellationEffectiveAt && (
   575→   575→              <Badge
   576→   576→                variant="outline"
   577→   577→                className="text-[9px] border-foreground/40"
   578→   578→                title={`Gekündigt am ${formatDate(license.cancelledAt!)}`}
   579→   579→              >
   580→   580→                Gekündigt → {formatDate(license.cancellationEffectiveAt)}
   581→   581→              </Badge>
   582→   582→            )}
   583→   583→            {license.environment === 'demo' && (
   584→   584→              <Badge variant="outline" className="text-[9px]">Demo</Badge>
   585→   585→            )}
   586→   586→          </div>
   587→   587→        </td>
   588→   588→        <td className="px-4 py-2.5 text-xs">
   589→   589→          {license.validUntil ? (
   590→   590→            <span className={cn(valid !== null && valid < 0 && 'text-foreground')}>
   591→   591→              {formatDate(license.validUntil)}
   592→   592→              {valid !== null && (
   593→   593→                <span className="ml-1 text-[10px] text-muted-foreground">
   594→   594→                  {valid < 0 ? `(${Math.abs(valid)} d vorbei)` : `(${valid} d)`}
   595→   595→                </span>
   596→   596→              )}
   597→   597→            </span>
   598→   598→          ) : (
   599→   599→            <span className="text-muted-foreground">—</span>
   600→   600→          )}
   601→   601→        </td>
   602→   602→        <td className="px-4 py-2.5">
   603→   603→          <div className="flex items-center gap-1">
   604→   604→            {canCancel && (
   605→   605→              <Button size="sm" variant="outline" className="h-7 gap-1 text-[10px]" onClick={() => onCancel(license)}>
   606→   606→                <Ban className="size-3" />Kündigen
   607→   607→              </Button>
   608→   608→            )}
   609→   609→            {license.status === 'active' && !cancelled && (
   610→   610→              <Button size="sm" variant="ghost" className="h-7 px-2 text-[10px]" onClick={() => onSuspend(license)} title="Suspendieren">
   611→   611→                <PauseCircle className="size-3" />
   612→   612→              </Button>
   613→   613→            )}
   614→   614→            {canReactivate && (
   615→   615→              <Button size="sm" variant="outline" className="h-7 gap-1 text-[10px]" onClick={() => onPromote(license)} title="Reaktivieren">
   616→   616→                <RotateCcw className="size-3" />Reaktivieren
   617→   617→              </Button>
   618→   618→            )}
   619→   619→            {license.environment === 'demo' && !cancelled && (
   620→   620→              <Button size="sm" variant="ghost" className="h-7 px-2 text-[10px]" onClick={() => onPromote(license)} title="Demo → Produktion">
   621→   621→                <Play className="size-3" />
   622→   622→              </Button>
   623→   623→            )}
   624→   624→          </div>
   625→   625→        </td>
   626→   626→      </tr>
   627→   627→
   628→   628→      {/* Expanded module rows */}
   629→   629→      {open && hasModules && (
   630→   630→        <tr className="bg-muted/20">
   631→   631→          <td colSpan={8} className="px-4 pb-3 pt-1">
   632→   632→            <div className="rounded-md border border-border bg-background">
   633→   633→              <table className="w-full text-xs">
   634→   634→                <thead className="border-b border-border text-left text-[10px] uppercase tracking-wide text-muted-foreground">
   635→   635→                  <tr>
   636→   636→                    <th className="px-3 py-2 font-medium">Modul</th>
   637→   637→                    <th className="px-3 py-2 font-medium">Schlüssel</th>
   638→   638→                    <th className="px-3 py-2 font-medium">Billing</th>
   639→   639→                    <th className="px-3 py-2 font-medium">Status</th>
   640→   640→                    <th className="px-3 py-2 font-medium">Gültig bis</th>
   641→   641→                  </tr>
   642→   642→                </thead>
   643→   643→                <tbody className="divide-y divide-border">
   644→   644→                  {license.modules.map((m) => {
   645→   645→                    const mst = LIC_STATUS[m.status] ?? LIC_STATUS.active
   646→   646→                    const MIcon = mst.icon
   647→   647→                    const mInTrial = !!m.trialEndsAt && new Date(m.trialEndsAt) > new Date()
   648→   648→                    const mCancelled = !!m.cancelledAt || m.status === 'cancelled'
   649→   649→                    return (
   650→   650→                      <tr key={m.id}>
   651→   651→                        <td className="px-3 py-2 font-medium">{m.module ?? '—'}</td>
   652→   652→                        <td className="px-3 py-2">
   653→   653→                          <code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">{m.key}</code>
   654→   654→                        </td>
   655→   655→                        <td className="px-3 py-2">
   656→   656→                          {m.priceCents > 0 ? (
   657→   657→                            <span className="tabular-nums text-muted-foreground">
   658→   658→                              {formatEuroCompact(m.netPriceCents)}
   659→   659→                              <span className="ml-0.5 text-[9px]">/{m.billingCycle === 'yearly' ? 'Jahr' : 'Mon.'}</span>
   660→   660→                            </span>
   661→   661→                          ) : (
   662→   662→                            <Badge variant="outline" className="text-[9px]">via Core</Badge>
   663→   663→                          )}
   664→   664→                        </td>
   665→   665→                        <td className="px-3 py-2">
   666→   666→                          <Badge
   667→   667→                            variant="outline"
   668→   668→                            className={cn(
   669→   669→                              'gap-1 text-[9px]',
   670→   670→                              mCancelled ? 'border-foreground/40' : mInTrial ? 'border-foreground/30' : ''
   671→   671→                            )}
   672→   672→                          >
   673→   673→                            <MIcon className="size-2.5" />{mst.label}
   674→   674→                          </Badge>
   675→   675→                          {mInTrial && (
   676→   676→                            <Badge variant="outline" className="ml-1 gap-1 text-[9px] border-foreground/30">
   677→   677→                              <FlaskConical className="size-2.5" />Trial
   678→   678→                            </Badge>
   679→   679→                          )}
   680→   680→                        </td>
   681→   681→                        <td className="px-3 py-2 text-muted-foreground">
   682→   682→                          {m.validUntil ? formatDate(m.validUntil) : '—'}
   683→   683→                        </td>
   684→   684→                      </tr>
   685→   685→                    )
   686→   686→                  })}
   687→   687→                </tbody>
   688→   688→              </table>
   689→   689→            </div>
   690→   690→          </td>
   691→   691→        </tr>
   692→   692→      )}
   693→   693→    </>
   694→   694→  )
   695→   695→}
   696→   696→
   697→   697→// ---------------------------------------------------------------------------
   698→   698→// Promote / Reactivate helper
   699→   699→// ---------------------------------------------------------------------------
   700→   700→
   701→   701→async function promoteLicense(license: License, refetch: () => void) {
   702→   702→  const t = toast.loading('Lizenz wird reaktiviert…')
   703→   703→  try {
   704→   704→    const r = await fetch(`/api/aos/licenses/${license.id}/promote`, {
   705→   705→      method: 'PATCH',
   706→   706→      headers: { 'content-type': 'application/json' },
   707→   707→      body: JSON.stringify({ environment: 'production' }),
   708→   708→    })
   709→   709→    if (!r.ok) {
   710→   710→      const e = await r.json().catch(() => ({}))
   711→   711→      throw new Error(e.error || 'promote failed')
   712→   712→    }
   713→   713→    toast.success('Lizenz reaktiviert', { id: t })
   714→   714→    refetch()
   715→   715→  } catch (err) {
   716→   716→    toast.error(err instanceof Error ? err.message : 'Reaktivierung fehlgeschlagen', { id: t })
   717→   717→  }
   718→   718→}
   719→   719→
   720→   720→// ---------------------------------------------------------------------------
   721→   721→// Create License Dialog — with Billing section + live pricing preview
   722→   722→// ---------------------------------------------------------------------------
   723→   723→
   724→   724→function CreateLicenseDialog({
   725→   725→  open,
   726→   726→  onOpenChange,
   727→   727→  companies,
   728→   728→  onCreated,
   729→   729→}: {
   730→   730→  open: boolean
   731→   731→  onOpenChange: (o: boolean) => void
   732→   732→  companies: CompanyLite[]
   733→   733→  onCreated: () => void
   734→   734→}) {
   735→   735→  const [companyId, setCompanyId] = useState('')
   736→   736→  const [product, setProduct] = useState('aos-cms')
   737→   737→  const [plan, setPlan] = useState<'starter' | 'pro' | 'enterprise'>('pro')
   738→   738→  const [environment, setEnvironment] = useState<'demo' | 'production'>('production')
   739→   739→  const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly')
   740→   740→  const [monthlyPriceCents, setMonthlyPriceCents] = useState<string>('')
   741→   741→  const [startTrial, setStartTrial] = useState(true)
   742→   742→  const [maxActivations, setMaxActivations] = useState(1)
   743→   743→  const [saving, setSaving] = useState(false)
   744→   744→
   745→   745→  // Default trial ON for production, OFF for demo.
   746→   746→  useEffect(() => {
   747→   747→    if (environment === 'demo') setStartTrial(false)
   748→   748→    else setStartTrial(true)
   749→   749→  }, [environment])
   750→   750→
   751→   751→  // Pick first company when the dialog opens.
   752→   752→  useEffect(() => {
   753→   753→    if (open && companies[0] && !companyId) setCompanyId(companies[0].id)
   754→   754→  }, [open, companies, companyId])
   755→   755→
   756→   756→  // Reset everything when the dialog closes.
   757→   757→  useEffect(() => {
   758→   758→    if (open) return
   759→   759→    setCompanyId('')
   760→   760→    setProduct('aos-cms')
   761→   761→    setPlan('pro')
   762→   762→    setEnvironment('production')
   763→   763→    setBillingCycle('monthly')
   764→   764→    setMonthlyPriceCents('')
   765→   765→    setStartTrial(true)
   766→   766→    setMaxActivations(1)
   767→   767→  }, [open])
   768→   768→
   769→   769→  const selectedCompany = companies.find((c) => c.id === companyId) ?? null
   770→   770→
   771→   771→  // Live pricing preview — mirrors the backend computePricing() formula.
   772→   772→  const pricing = useMemo(() => {
   773→   773→    const monthlyCents = monthlyPriceCents
   774→   774→      ? Math.max(0, parseInt(monthlyPriceCents, 10) || 0)
   775→   775→      : PLAN_PRICES[plan] ?? 0
   776→   776→    const baseCents = billingCycle === 'yearly' ? monthlyCents * 12 : monthlyCents
   777→   777→    const yearlyPct = billingCycle === 'yearly' ? YEARLY_DISCOUNT_PCT : 0
   778→   778→    let agencyPct = 0
   779→   779→    if (selectedCompany?.parentAgencyId) {
   780→   780→      const agency = companies.find((c) => c.id === selectedCompany.parentAgencyId)
   781→   781→      if (agency?.isAgency && agency.agencyStatus === 'approved') {
   782→   782→        agencyPct = agency.agencyDiscountPct ?? 0
   783→   783→      }
   784→   784→    }
   785→   785→    const totalPct = agencyPct + yearlyPct
   786→   786→    const discountAmount = Math.round((baseCents * totalPct) / 100)
   787→   787→    const netPriceCents = baseCents - discountAmount
   788→   788→    return { monthlyCents, baseCents, agencyPct, yearlyPct, totalPct, discountAmount, netPriceCents }
   789→   789→  }, [monthlyPriceCents, billingCycle, plan, selectedCompany, companies])
   790→   790→
   791→   791→  const submit = async () => {
   792→   792→    if (!companyId) { toast.error('Firma auswählen'); return }
   793→   793→    if (!product.trim()) { toast.error('Produkt eingeben'); return }
   794→   794→    setSaving(true)
   795→   795→    const t = toast.loading('Lizenz wird ausgestellt…')
   796→   796→    try {
   797→   797→      const body: Record<string, unknown> = {
   798→   798→        companyId,
   799→   799→        product: product.trim(),
   800→   800→        plan,
   801→   801→        environment,
   802→   802→        billingCycle,
   803→   803→        maxActivations,
   804→   804→        startTrial,
   805→   805→      }
   806→   806→      const parsed = monthlyPriceCents ? parseInt(monthlyPriceCents, 10) : NaN
   807→   807→      if (!Number.isNaN(parsed) && parsed > 0) body.monthlyPriceCents = parsed
   808→   808→      const r = await fetch('/api/aos/licenses', {
   809→   809→        method: 'POST',
   810→   810→        headers: { 'content-type': 'application/json' },
   811→   811→        body: JSON.stringify(body),
   812→   812→      })
   813→   813→      const res = await r.json()
   814→   814→      if (!r.ok) throw new Error(res.error || 'create failed')
   815→   815→      const pricingEcho = res.pricing
   816→   816→      const cycleLabel = billingCycle === 'yearly' ? 'Jahr' : 'Monat'
   817→   817→      const netCents = typeof pricingEcho?.netPriceCents === 'number'
   818→   818→        ? pricingEcho.netPriceCents
   819→   819→        : pricing.netPriceCents
   820→   820→      const discPct = typeof pricingEcho?.totalDiscountPct === 'number'
   821→   821→        ? pricingEcho.totalDiscountPct
   822→   822→        : pricing.totalPct
   823→   823→      const netEuro = (netCents / 100).toLocaleString('de-DE', {
   824→   824→        minimumFractionDigits: 2,
   825→   825→        maximumFractionDigits: 2,
   826→   826→      })
   827→   827→      toast.success(
   828→   828→        `Lizenz ausgestellt — ${netEuro} €/${cycleLabel} (inkl. ${discPct}% Rabatt)`,
   829→   829→        { id: t, duration: 6000 }
   830→   830→      )
   831→   831→      onOpenChange(false)
   832→   832→      onCreated()
   833→   833→    } catch (err) {
   834→   834→      toast.error(err instanceof Error ? err.message : 'Ausstellung fehlgeschlagen', { id: t })
   835→   835→    } finally {
   836→   836→      setSaving(false)
   837→   837→    }
   838→   838→  }
   839→   839→
   840→   840→  return (
   841→   841→    <Dialog open={open} onOpenChange={onOpenChange}>
   842→   842→      <DialogContent className="sm:max-w-lg">
   843→   843→        <DialogHeader>
   844→   844→          <DialogTitle>Lizenz ausstellen</DialogTitle>
   845→   845→          <DialogDescription>
   846→   846→            Stellt eine neue Lizenz aus. Billing, Probezeit und Kündigungsfrist (90 Tage) werden automatisch konfiguriert.
   847→   847→          </DialogDescription>
   848→   848→        </DialogHeader>
   849→   849→        <div className="space-y-3">
   850→   850→          {/* Customer */}
   851→   851→          <div className="space-y-1.5">
   852→   852→            <Label className="text-xs">Firma</Label>
   853→   853→            <Select value={companyId} onValueChange={setCompanyId}>
   854→   854→              <SelectTrigger className="w-full"><SelectValue placeholder="Firma wählen" /></SelectTrigger>
   855→   855→              <SelectContent>
   856→   856→                {companies.map((c) => (
   857→   857→                  <SelectItem key={c.id} value={c.id}>
   858→   858→                    {c.name}
   859→   859→                    {c.isAgency && c.agencyStatus === 'approved' && ' (Agentur)'}
   860→   860→                  </SelectItem>
   861→   861→                ))}
   862→   862→              </SelectContent>
   863→   863→            </Select>
   864→   864→          </div>
   865→   865→          {/* Product + Plan */}
   866→   866→          <div className="grid grid-cols-2 gap-3">
   867→   867→            <div className="space-y-1.5">
   868→   868→              <Label className="text-xs">Produkt</Label>
   869→   869→              <Input value={product} onChange={(e) => setProduct(e.target.value)} placeholder="z.B. aos-cms" />
   870→   870→            </div>
   871→   871→            <div className="space-y-1.5">
   872→   872→              <Label className="text-xs">Plan</Label>
   873→   873→              <Select value={plan} onValueChange={(v) => setPlan(v as 'starter' | 'pro' | 'enterprise')}>
   874→   874→                <SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
   875→   875→                <SelectContent>
   876→   876→                  <SelectItem value="starter">Starter (29 €/Mon.)</SelectItem>
   877→   877→                  <SelectItem value="pro">Pro (50 €/Mon.)</SelectItem>
   878→   878→                  <SelectItem value="enterprise">Enterprise (150 €/Mon.)</SelectItem>
   879→   879→                </SelectContent>
   880→   880→              </Select>
   881→   881→            </div>
   882→   882→          </div>
   883→   883→          {/* Environment + Activations */}
   884→   884→          <div className="grid grid-cols-2 gap-3">
   885→   885→            <div className="space-y-1.5">
   886→   886→              <Label className="text-xs">Umgebung</Label>
   887→   887→              <Select value={environment} onValueChange={(v) => setEnvironment(v as 'demo' | 'production')}>
   888→   888→                <SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
   889→   889→                <SelectContent>
   890→   890→                  <SelectItem value="production">Produktion</SelectItem>
   891→   891→                  <SelectItem value="demo">Demo (14 d, Wasserzeichen)</SelectItem>
   892→   892→                </SelectContent>
   893→   893→              </Select>
   894→   894→            </div>
   895→   895→            <div className="space-y-1.5">
   896→   896→              <Label className="text-xs">Max. Aktivierungen</Label>
   897→   897→              <Input
   898→   898→                type="number"
   899→   899→                min={1}
   900→   900→                max={100}
   901→   901→                value={maxActivations}
   902→   902→                onChange={(e) => setMaxActivations(Math.max(1, parseInt(e.target.value, 10) || 1))}
   903→   903→              />
   904→   904→            </div>
   905→   905→          </div>
   906→   906→
   907→   907→          {/* Billing section */}
   908→   908→          <div className="space-y-3 rounded-md border border-border p-3">
   909→   909→            <div className="flex items-center gap-2">
   910→   910→              <CircleDollarSign className="size-3.5 text-muted-foreground" />
   911→   911→              <span className="text-xs font-semibold uppercase tracking-wide">Billing</span>
   912→   912→            </div>
   913→   913→            <div className="grid grid-cols-2 gap-3">
   914→   914→              <div className="space-y-1.5">
   915→   915→                <Label className="text-xs">Abrechnung</Label>
   916→   916→                <Select value={billingCycle} onValueChange={(v) => setBillingCycle(v as 'monthly' | 'yearly')}>
   917→   917→                  <SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
   918→   918→                  <SelectContent>
   919→   919→                    <SelectItem value="monthly">Monatlich</SelectItem>
   920→   920→                    <SelectItem value="yearly">Jährlich</SelectItem>
   921→   921→                  </SelectContent>
   922→   922→                </Select>
   923→   923→                {billingCycle === 'yearly' && (
   924→   924→                  <p className="text-[10px] text-muted-foreground">10% Jahresrabatt wird auf den Jahresbetrag angewendet.</p>
   925→   925→                )}
   926→   926→              </div>
   927→   927→              <div className="space-y-1.5">
   928→   928→                <Label className="text-xs">Monatspreis (Cents)</Label>
   929→   929→                <Input
   930→   930→                  type="number"
   931→   931→                  min={0}
   932→   932→                  value={monthlyPriceCents}
   933→   933→                  onChange={(e) => setMonthlyPriceCents(e.target.value)}
   934→   934→                  placeholder="z.B. 5000 (= 50€)"
   935→   935→                />
   936→   936→                <p className="text-[10px] text-muted-foreground">
   937→   937→                  Standard: 29€ (Starter), 50€ (Pro), 150€ (Enterprise). Überschreiben für Custom-Preis.
   938→   938→                </p>
   939→   939→              </div>
   940→   940→            </div>
   941→   941→            <div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 p-2.5">
   942→   942→              <div>
   943→   943→                <Label className="text-xs">14-Tage-Testphase starten</Label>
   944→   944→                <p className="text-[10px] text-muted-foreground">Tag 1–14 kostenlos kündbar. Tag 15 beginnt das kostenpflichtige Abo.</p>
   945→   945→              </div>
   946→   946→              <Switch
   947→   947→                checked={startTrial}
   948→   948→                onCheckedChange={setStartTrial}
   949→   949→                disabled={environment === 'demo'}
   950→   950→              />
   951→   951→            </div>
   952→   952→
   953→   953→            {/* Live pricing preview */}
   954→   954→            <div className="rounded-md border border-border bg-background p-2.5 text-xs">
   955→   955→              <div className="flex items-center justify-between">
   956→   956→                <span className="text-muted-foreground">Basispreis ({billingCycle === 'yearly' ? 'Jahr' : 'Monat'})</span>
   957→   957→                <span className="tabular-nums">{formatEuro(pricing.baseCents)}</span>
   958→   958→              </div>
   959→   959→              {pricing.agencyPct > 0 && (
   960→   960→                <div className="mt-1 flex items-center justify-between">
   961→   961→                  <span className="text-muted-foreground">Agentur-Rabatt ({pricing.agencyPct}%)</span>
   962→   962→                  <span className="tabular-nums text-foreground">−{formatEuro(Math.round((pricing.baseCents * pricing.agencyPct) / 100))}</span>
   963→   963→                </div>
   964→   964→              )}
   965→   965→              {pricing.yearlyPct > 0 && (
   966→   966→                <div className="mt-1 flex items-center justify-between">
   967→   967→                  <span className="text-muted-foreground">Jahresrabatt ({pricing.yearlyPct}%)</span>
   968→   968→                  <span className="tabular-nums text-foreground">−{formatEuro(Math.round((pricing.baseCents * pricing.yearlyPct) / 100))}</span>
   969→   969→                </div>
   970→   970→              )}
   971→   971→              {pricing.totalPct > 0 && (
   972→   972→                <div className="mt-1 flex items-center justify-between text-muted-foreground">
   973→   973→                  <span>Gesamtrabatt</span>
   974→   974→                  <span>{pricing.totalPct}%</span>
   975→   975→                </div>
   976→   976→              )}
   977→   977→              <div className="mt-2 flex items-center justify-between border-t border-border pt-2 font-medium">
   978→   978→                <span>Netto / {billingCycle === 'yearly' ? 'Jahr' : 'Monat'}</span>
   979→   979→                <span className="tabular-nums">{formatEuro(pricing.netPriceCents)}</span>
   980→   980→              </div>
   981→   981→            </div>
   982→   982→          </div>
   983→   983→        </div>
   984→   984→        <DialogFooter>
   985→   985→          <Button variant="outline" onClick={() => onOpenChange(false)}>Abbrechen</Button>
   986→   986→          <Button onClick={submit} disabled={saving} className="gap-1.5">
   987→   987→            {saving && <Loader2 className="size-3.5 animate-spin" />}
   988→   988→            Ausstellen
   989→   989→          </Button>
   990→   990→        </DialogFooter>
   991→   991→      </DialogContent>
   992→   992→    </Dialog>
   993→   993→  )
   994→   994→}
   995→   995→
   996→   996→// ---------------------------------------------------------------------------
   997→   997→// Suspend Dialog
   998→   998→// ---------------------------------------------------------------------------
   999→   999→
  1000→  1000→function SuspendDialog({
  1001→  1001→  license,
  1002→  1002→  onOpenChange,
  1003→  1003→  onDone,
  1004→  1004→}: {
  1005→  1005→  license: License | null
  1006→  1006→  onOpenChange: (o: boolean) => void
  1007→  1007→  onDone: () => void
  1008→  1008→}) {
  1009→  1009→  const [reason, setReason] = useState('')
  1010→  1010→  const [saving, setSaving] = useState(false)
  1011→  1011→
  1012→  1012→  useEffect(() => {
  1013→  1013→    if (!license) setReason('')
  1014→  1014→  }, [license])
  1015→  1015→
  1016→  1016→  const submit = async () => {
  1017→  1017→    if (!license) return
  1018→  1018→    if (!reason.trim()) { toast.error('Grund erforderlich'); return }
  1019→  1019→    setSaving(true)
  1020→  1020→    const t = toast.loading('Lizenz wird suspendiert…')
  1021→  1021→    try {
  1022→  1022→      const r = await fetch(`/api/aos/licenses/${license.id}/suspend`, {
  1023→  1023→        method: 'PATCH',
  1024→  1024→        headers: { 'content-type': 'application/json' },
  1025→  1025→        body: JSON.stringify({ reason: reason.trim() }),
  1026→  1026→      })
  1027→  1027→      const res = await r.json()
  1028→  1028→      if (!r.ok) throw new Error(res.error || 'suspend failed')
  1029→  1029→      toast.success(
  1030→  1030→        `Lizenz suspendiert${res.suspendedModules ? ` (+${res.suspendedModules} Module)` : ''}`,
  1031→  1031→        { id: t }
  1032→  1032→      )
  1033→  1033→      onOpenChange(false)
  1034→  1034→      onDone()
  1035→  1035→    } catch (err) {
  1036→  1036→      toast.error(err instanceof Error ? err.message : 'Suspension fehlgeschlagen', { id: t })
  1037→  1037→    } finally {
  1038→  1038→      setSaving(false)
  1039→  1039→    }
  1040→  1040→  }
  1041→  1041→
  1042→  1042→  return (
  1043→  1043→    <Dialog open={!!license} onOpenChange={onOpenChange}>
  1044→  1044→      <DialogContent className="sm:max-w-md">
  1045→  1045→        <DialogHeader>
  1046→  1046→          <DialogTitle>Lizenz suspendieren</DialogTitle>
  1047→  1047→          <DialogDescription>
  1048→  1048→            Suspendiert die Lizenz (und alle Module). Aktivierte Installationen verweigern den Dienst bis zur Reaktivierung.
  1049→  1049→          </DialogDescription>
  1050→  1050→        </DialogHeader>
  1051→  1051→        {license && (
  1052→  1052→          <div className="space-y-3">
  1053→  1053→            <div className="rounded-md border border-border bg-muted/30 p-2.5 text-xs">
  1054→  1054→              <p className="font-medium">{license.product} — {license.plan}</p>
  1055→  1055→              <p className="mt-0.5 text-muted-foreground">{license.company.name}</p>
  1056→  1056→              <code className="mt-1 inline-block rounded bg-muted px-1 py-0.5 font-mono text-[10px]">{license.key}</code>
  1057→  1057→            </div>
  1058→  1058→            <div className="space-y-1.5">
  1059→  1059→              <Label className="text-xs">Grund</Label>
  1060→  1060→              <Textarea
  1061→  1061→                value={reason}
  1062→  1062→                onChange={(e) => setReason(e.target.value)}
  1063→  1063→                placeholder="z.B. Zahlungsverzug Mahnstufe 3, Missbrauch, Kunden-Anfrage…"
  1064→  1064→                rows={3}
  1065→  1065→              />
  1066→  1066→            </div>
  1067→  1067→          </div>
  1068→  1068→        )}
  1069→  1069→        <DialogFooter>
  1070→  1070→          <Button variant="outline" onClick={() => onOpenChange(false)}>Abbrechen</Button>
  1071→  1071→          <Button variant="destructive" onClick={submit} disabled={saving} className="gap-1.5">
  1072→  1072→            {saving && <Loader2 className="size-3.5 animate-spin" />}
  1073→  1073→            Suspendieren
  1074→  1074→          </Button>
  1075→  1075→        </DialogFooter>
  1076→  1076→      </DialogContent>
  1077→  1077→    </Dialog>
  1078→  1078→  )
  1079→  1079→}
  1080→  1080→
  1081→  1081→// ---------------------------------------------------------------------------
  1082→  1082→// Cancel Dialog — slide-to-confirm (SlideCommit)
  1083→  1083→// ---------------------------------------------------------------------------
  1084→  1084→
  1085→  1085→function CancelDialog({
  1086→  1086→  license,
  1087→  1087→  onOpenChange,
  1088→  1088→  onDone,
  1089→  1089→}: {
  1090→  1090→  license: License | null
  1091→  1091→  onOpenChange: (o: boolean) => void
  1092→  1092→  onDone: () => void
  1093→  1093→}) {
  1094→  1094→  const [reason, setReason] = useState('')
  1095→  1095→  const [committing, setCommitting] = useState(false)
  1096→  1096→
  1097→  1097→  useEffect(() => {
  1098→  1098→    if (!license) {
  1099→  1099→      setReason('')
  1100→  1100→      setCommitting(false)
  1101→  1101→    }
  1102→  1102→  }, [license])
  1103→  1103→
  1104→  1104→  if (!license) return null
  1105→  1105→
  1106→  1106→  const inTrial = !!license.trialEndsAt && new Date(license.trialEndsAt) > new Date()
  1107→  1107→  // Estimated effective date (mirrors the backend formula for preview only —
  1108→  1108→  // the backend returns the authoritative effectiveAt in the response).
  1109→  1109→  const effectiveAt = inTrial
  1110→  1110→    ? new Date()
  1111→  1111→    : new Date(Date.now() + (license.noticePeriodDays || 90) * 86400000)
  1112→  1112→
  1113→  1113→  const handleCommit = async () => {
  1114→  1114→    setCommitting(true)
  1115→  1115→    const t = toast.loading('Kündigung wird eingereicht…')
  1116→  1116→    try {
  1117→  1117→      const r = await fetch(`/api/aos/licenses/${license.id}/cancel`, {
  1118→  1118→        method: 'PATCH',
  1119→  1119→        headers: { 'content-type': 'application/json' },
  1120→  1120→        body: JSON.stringify({ reason: reason.trim() || undefined }),
  1121→  1121→      })
  1122→  1122→      const res = await r.json()
  1123→  1123→      if (!r.ok) throw new Error(res.error || 'cancel failed')
  1124→  1124→      toast.success(res.message ?? 'Kündigung eingegangen', { id: t, duration: 6000 })
  1125→  1125→      onOpenChange(false)
  1126→  1126→      onDone()
  1127→  1127→    } catch (err) {
  1128→  1128→      toast.error(err instanceof Error ? err.message : 'Kündigung fehlgeschlagen', { id: t })
  1129→  1129→    } finally {
  1130→  1130→      setCommitting(false)
  1131→  1131→    }
  1132→  1132→  }
  1133→  1133→
  1134→  1134→  return (
  1135→  1135→    <Dialog open={!!license} onOpenChange={onOpenChange}>
  1136→  1136→      <DialogContent className="sm:max-w-md">
  1137→  1137→        <DialogHeader>
  1138→  1138→          <DialogTitle className="flex items-center gap-2">
  1139→  1139→            <Ban className="size-4" />
  1140→  1140→            Lizenz kündigen
  1141→  1141→          </DialogTitle>
  1142→  1142→          <DialogDescription>
  1143→  1143→            Kündigt das Abonnement. Innerhalb der 14-Tage-Testphase wird die Kündigung sofort wirksam (keine Abbuchung). Nach der Testphase gilt die gesetzliche Kündigungsfrist von 90 Tagen.
  1144→  1144→          </DialogDescription>
  1145→  1145→        </DialogHeader>
  1146→  1146→        <div className="space-y-3">
  1147→  1147→          <div className="rounded-md border border-border bg-muted/30 p-2.5 text-xs">
  1148→  1148→            <p className="font-medium">{license.product} — {license.plan}</p>
  1149→  1149→            <p className="mt-0.5 text-muted-foreground">{license.company.name}</p>
  1150→  1150→            <code className="mt-1 inline-block rounded bg-muted px-1 py-0.5 font-mono text-[10px]">{license.key}</code>
  1151→  1151→          </div>
  1152→  1152→
  1153→  1153→          {/* Trial status banner */}
  1154→  1154→          <div
  1155→  1155→            className={cn(
  1156→  1156→              'flex items-start gap-2 rounded-md border p-2.5 text-xs',
  1157→  1157→              inTrial ? 'border-foreground/30 bg-foreground/5' : 'border-foreground/40 bg-foreground/5'
  1158→  1158→            )}
  1159→  1159→          >
  1160→  1160→            <FlaskConical className="mt-0.5 size-3.5 shrink-0 text-foreground" />
  1161→  1161→            <div>
  1162→  1162→              {inTrial ? (
  1163→  1163→                <>
  1164→  1164→                  <p className="font-medium">In Probezeit — sofortige Kündigung</p>
  1165→  1165→                  <p className="mt-0.5 text-muted-foreground">
  1166→  1166→                    Sie befinden sich in der 14-Tage-Testphase — Kündigung wird sofort wirksam, keine Abbuchung.
  1167→  1167→                  </p>
  1168→  1168→                </>
  1169→  1169→              ) : (
  1170→  1170→                <>
  1171→  1171→                  <p className="font-medium">Kündigungsfrist: 90 Tage</p>
  1172→  1172→                  <p className="mt-0.5 text-muted-foreground">
  1173→  1173→                    Kündigung frist: 90 Tage. Wirksam zum{' '}
  1174→  1174→                    <span className="font-medium">{formatDate(effectiveAt)}</span>{' '}
  1175→  1175→                    <span className="text-muted-foreground">({formatRelative(effectiveAt)}).</span>
  1176→  1176→                  </p>
  1177→  1177→                </>
  1178→  1178→              )}
  1179→  1179→            </div>
  1180→  1180→          </div>
  1181→  1181→
  1182→  1182→          <div className="space-y-1.5">
  1183→  1183→            <Label className="text-xs">Kündigungsgrund (optional)</Label>
  1184→  1184→            <Textarea
  1185→  1185→              value={reason}
  1186→  1186→              onChange={(e) => setReason(e.target.value)}
  1187→  1187→              placeholder="z.B. Projektabbruch, Umzug auf andere Lösung, Kosten…"
  1188→  1188→              rows={3}
  1189→  1189→            />
  1190→  1190→          </div>
  1191→  1191→
  1192→  1192→          <SlideCommit
  1193→  1193→            label="Kündigung bestätigen →"
  1194→  1194→            onCommit={handleCommit}
  1195→  1195→            loading={committing}
  1196→  1196→          />
  1197→  1197→        </div>
  1198→  1198→      </DialogContent>
  1199→  1199→    </Dialog>
  1200→  1200→  )
  1201→  1201→}
  1202→  1202→