1→'use client' 2→ 3→import { useMemo, useState } from 'react' 4→import { 5→ Users, 6→ Building2, 7→ Globe, 8→ MapPin, 9→ Clock, 10→ Percent, 11→ CheckCircle2, 12→ XCircle, 13→ History, 14→ Inbox, 15→ Mail, 16→ Loader2, 17→ ChevronRight, 18→ ShoppingBag, 19→} from 'lucide-react' 20→import { useFetch } from '@/hooks/use-fetch' 21→import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 22→import { Badge } from '@/components/ui/badge' 23→import { Button } from '@/components/ui/button' 24→import { Input } from '@/components/ui/input' 25→import { Label } from '@/components/ui/label' 26→import { Textarea } from '@/components/ui/textarea' 27→import { Skeleton } from '@/components/ui/skeleton' 28→import { 29→ Dialog, 30→ DialogContent, 31→ DialogDescription, 32→ DialogFooter, 33→ DialogHeader, 34→ DialogTitle, 35→} from '@/components/ui/dialog' 36→import { 37→ Sheet, 38→ SheetContent, 39→ SheetDescription, 40→ SheetHeader, 41→ SheetTitle, 42→} from '@/components/ui/sheet' 43→import { 44→ Collapsible, 45→ CollapsibleContent, 46→ CollapsibleTrigger, 47→} from '@/components/ui/collapsible' 48→import { PageHeader } from '../primitives' 49→import { FuseButton } from '@/components/FuseButton' 50→import { formatRelative, formatDate, formatDateTime } from '@/lib/format' 51→import { cn } from '@/lib/utils' 52→import { toast } from 'sonner' 53→ 54→/* ----------------------------- types ----------------------------- */ 55→ 56→interface AgencyCompany { 57→ id: string 58→ name: string 59→ email: string | null 60→ website: string | null 61→ city: string | null 62→ isAgency: boolean 63→ agencyStatus: string | null 64→ agencyDiscountPct: number | null 65→} 66→ 67→interface AgencyApplication { 68→ id: string 69→ companyId: string 70→ status: 'pending' | 'approved' | 'rejected' 71→ discountPct: number 72→ businessModel?: string | null 73→ expectedClientVolume?: string | null 74→ referredBy?: string | null 75→ reviewedBy?: string | null 76→ reviewedAt?: string | null 77→ rejectReason?: string | null 78→ appliedAt: string 79→ createdAt: string 80→ updatedAt: string 81→ company: AgencyCompany 82→} 83→ 84→interface AgencyClient { 85→ id: string 86→ name: string 87→ email: string | null 88→} 89→ 90→interface ApprovedAgency extends AgencyCompany { 91→ agencyDiscountPct: number 92→ agencyApprovedAt: string 93→ agencyClients: AgencyClient[] 94→ _count: { agencyClients: number; licenses: number } 95→} 96→ 97→interface AgencyData { 98→ applications: { 99→ pending: AgencyApplication[] 100→ reviewed: AgencyApplication[] 101→ } 102→ agencies: ApprovedAgency[] 103→ stats: { pending: number; approved: number; totalApplications: number } 104→} 105→ 106→const BASE_PRICE_EUR = 50 // 50 €/Monat base price for preview 107→ 108→/* ----------------------------- view ----------------------------- */ 109→ 110→export function AgencyView() { 111→ const { data, loading, refetch } = useFetch('/api/aos/agency') 112→ 113→ if (loading || !data) { 114→ return ( 115→
116→ 117→
118→ {Array.from({ length: 4 }).map((_, i) => ( 119→ 120→ ))} 121→
122→ 123→
124→ ) 125→ } 126→ 127→ const { applications, agencies, stats } = data 128→ const avgDiscount = 129→ agencies.length > 0 130→ ? Math.round( 131→ agencies.reduce((sum, a) => sum + (a.agencyDiscountPct ?? 0), 0) / agencies.length, 132→ ) 133→ : 0 134→ 135→ return ( 136→
137→ 141→ 142→ {/* Stats */} 143→
144→ 150→ 156→ 162→ 168→
169→ 170→ 171→ 172→ 173→
174→ ) 175→} 176→ 177→/* ----------------------------- stats card ----------------------------- */ 178→ 179→function StatCard({ 180→ label, 181→ value, 182→ icon: Icon, 183→ hint, 184→}: { 185→ label: string 186→ value: number | string 187→ icon: typeof Inbox 188→ hint?: string 189→}) { 190→ return ( 191→ 192→
193→

{label}

194→ 195→
196→

{value}

197→ {hint &&

{hint}

} 198→
199→ ) 200→} 201→ 202→/* ----------------------------- pending applications ----------------------------- */ 203→ 204→function PendingApplicationsSection({ 205→ applications, 206→ onChanged, 207→}: { 208→ applications: AgencyApplication[] 209→ onChanged: () => void 210→}) { 211→ const [approveTarget, setApproveTarget] = useState(null) 212→ const [rejectTarget, setRejectTarget] = useState(null) 213→ 214→ return ( 215→
216→
217→

218→ Bewerbende Reseller 219→ 220→ {applications.length} offen 221→ 222→

223→
224→ 225→ {applications.length === 0 ? ( 226→
227→
228→ 229→
230→
231→

Keine bewerbenden Anfragen

232→

233→ Neue Reseller-Bewerbungen erscheinen hier. 234→

235→
236→
237→ ) : ( 238→
239→ {applications.map((app) => ( 240→ 241→ 242→ {/* header */} 243→
244→
245→

{app.company.name}

246→
247→ {app.company.email && ( 248→ 249→ 250→ {app.company.email} 251→ 252→ )} 253→ {app.company.website && ( 254→ 255→ 256→ {app.company.website} 257→ 258→ )} 259→ {app.company.city && ( 260→ 261→ 262→ {app.company.city} 263→ 264→ )} 265→
266→
267→ 268→ 269→ {formatRelative(app.appliedAt)} 270→ 271→
272→ 273→ {/* application details */} 274→ {(app.businessModel || app.expectedClientVolume || app.referredBy) && ( 275→
276→ {app.businessModel && ( 277→ 278→ )} 279→ {app.expectedClientVolume && ( 280→ 281→ )} 282→ {app.referredBy && ( 283→ 284→ )} 285→
286→ )} 287→ 288→ {/* actions */} 289→
290→ 298→ 305→
306→
307→
308→ ))} 309→
310→ )} 311→ 312→ setApproveTarget(null)} 315→ onChanged={onChanged} 316→ /> 317→ setRejectTarget(null)} 320→ onChanged={onChanged} 321→ /> 322→
323→ ) 324→} 325→ 326→function Meta({ label, value }: { label: string; value: string }) { 327→ return ( 328→
329→

{label}

330→

{value}

331→
332→ ) 333→} 334→ 335→/* ----------------------------- approve dialog ----------------------------- */ 336→ 337→function ApproveDialog({ 338→ application, 339→ onClose, 340→ onChanged, 341→}: { 342→ application: AgencyApplication | null 343→ onClose: () => void 344→ onChanged: () => void 345→}) { 346→ const [discount, setDiscount] = useState(10) 347→ const [committing, setCommitting] = useState(false) 348→ 349→ // Reset state when dialog opens for a new application 350→ const handleOpenChange = (open: boolean) => { 351→ if (open) setDiscount(10) 352→ if (!open) { 353→ if (!committing) onClose() 354→ } 355→ } 356→ 357→ const net = useMemo(() => { 358→ const netEur = BASE_PRICE_EUR * (1 - discount / 100) 359→ return netEur.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) 360→ }, [discount]) 361→ 362→ const handleCommit = async () => { 363→ if (!application) return 364→ setCommitting(true) 365→ try { 366→ const r = await fetch(`/api/aos/agency/${application.id}/approve`, { 367→ method: 'PATCH', 368→ headers: { 'content-type': 'application/json' }, 369→ body: JSON.stringify({ discountPct: discount }), 370→ }) 371→ if (!r.ok) { 372→ const err = await r.json().catch(() => ({})) 373→ throw new Error(err.error || 'approve failed') 374→ } 375→ // Defer close + refetch so the FuseButton's done-state + toast can render. 376→ setTimeout(() => { 377→ onClose() 378→ onChanged() 379→ }, 350) 380→ } catch (err) { 381→ setCommitting(false) 382→ throw err 383→ } 384→ // Note: setCommitting(false) intentionally skipped on success — dialog will 385→ // close via the setTimeout above and the button's doneLabel will be shown. 386→ } 387→ 388→ return ( 389→ 390→ 391→ {application && ( 392→ <> 393→ 394→ 395→ 396→ Reseller-Bewerbung genehmigen 397→ 398→ 399→ {application.company.name} wird 400→ als Reseller freigeschaltet. Der Rabatt wird auf alle vom Reseller angelegten Kundenlizenzierungen 401→ angewendet. 402→ 403→ 404→ 405→
406→
407→
408→ 409→ 0 – 20 410→
411→ { 419→ const raw = parseInt(e.target.value, 10) 420→ if (Number.isNaN(raw)) { 421→ setDiscount(0) 422→ return 423→ } 424→ setDiscount(Math.min(20, Math.max(0, raw))) 425→ }} 426→ className="tabular-nums" 427→ /> 428→
429→ 430→ {/* live preview */} 431→
432→

433→ Preisvorschau 434→

435→

436→ {BASE_PRICE_EUR} €/Monat 437→ Basis 438→ → 439→ mit {discount}% Rabatt 440→ = 441→ 442→ {net} € 443→ 444→

445→
446→
447→ 448→ 449→ 452→ 459→ 460→ 461→ )} 462→
463→
464→ ) 465→} 466→ 467→/* ----------------------------- reject dialog ----------------------------- */ 468→ 469→function RejectDialog({ 470→ application, 471→ onClose, 472→ onChanged, 473→}: { 474→ application: AgencyApplication | null 475→ onClose: () => void 476→ onChanged: () => void 477→}) { 478→ const [reason, setReason] = useState('') 479→ const [submitting, setSubmitting] = useState(false) 480→ 481→ const handleOpenChange = (open: boolean) => { 482→ if (!open) { 483→ if (!submitting) { 484→ setReason('') 485→ onClose() 486→ } 487→ } 488→ } 489→ 490→ const submit = async () => { 491→ if (!application) return 492→ if (!reason.trim()) { 493→ toast.error('Bitte gib einen Ablehnungsgrund an.') 494→ return 495→ } 496→ setSubmitting(true) 497→ const t = toast.loading('Ablehnung wird gespeichert…') 498→ try { 499→ const r = await fetch(`/api/aos/agency/${application.id}/reject`, { 500→ method: 'PATCH', 501→ headers: { 'content-type': 'application/json' }, 502→ body: JSON.stringify({ reason: reason.trim() }), 503→ }) 504→ if (!r.ok) { 505→ const err = await r.json().catch(() => ({})) 506→ throw new Error(err.error || 'reject failed') 507→ } 508→ toast.success(`${application.company.name} abgelehnt`, { id: t }) 509→ setReason('') 510→ onClose() 511→ onChanged() 512→ } catch (err) { 513→ toast.error(err instanceof Error ? err.message : 'Ablehnung fehlgeschlagen', { id: t }) 514→ } finally { 515→ setSubmitting(false) 516→ } 517→ } 518→ 519→ return ( 520→ 521→ 522→ {application && ( 523→ <> 524→ 525→ 526→ 527→ Reseller-Bewerbung ablehnen 528→ 529→ 530→ {application.company.name} 531→ {' '}wird abgelehnt. Der Grund wird für den Bewerber sichtbar protokolliert. 532→ 533→ 534→ 535→
536→ 537→