veza/apps/web/src/components/admin/AdminTransfersView.tsx
senke 7b39efa176 fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- Fix 98 TypeScript errors across 37 files:
  - Service layer double-unwrapping (subscriptionService, distributionService, gearService)
  - Self-referencing variables in SearchPageResults
  - FeedView/ExploreView .posts→.items alignment
  - useQueueSync Zustand subscribe API
  - AdminAuditLogsView missing interface fields
  - Toast proxy type, interceptor type narrowing
  - 22 unused imports/variables removed
  - 5 storybook mock data fixes

- Align frontend API calls with backend endpoints:
  - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics)
  - Chat: chatService uses /conversations (was mock data), WS URL from backend token
  - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros)
  - Settings: suppress 2FA toast error when endpoint unavailable

- Fix marketplace products: seed uses 'active' status (was 'published')
- Enrich seed: admin follows all creators (feed has content)

- Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%)
  Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc.

- Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:18:49 +01:00

249 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Select } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
import { RefreshCcw, ChevronLeft, ChevronRight } from 'lucide-react';
import { commerceService, type SellerTransfer } from '@/services/commerceService';
import { useToast } from '@/components/feedback/ToastProvider';
const LIMIT = 20;
function statusBadgeClass(status: string): string {
switch (status) {
case 'completed':
return 'bg-success/20 text-success';
case 'failed':
return 'bg-destructive/20 text-destructive';
case 'permanently_failed':
return 'bg-muted text-muted-foreground';
case 'pending':
return 'bg-warning/20 text-warning';
case 'skipped':
return 'bg-primary/20 text-primary';
default:
return 'bg-muted text-muted-foreground';
}
}
function truncateId(id: string): string {
if (!id || id.length < 12) return id;
return `${id.slice(0, 8)}`;
}
export function AdminTransfersView() {
const { addToast } = useToast();
const [transfers, setTransfers] = useState<SellerTransfer[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [statusFilter, setStatusFilter] = useState<string>('all');
const [sellerIdFilter, setSellerIdFilter] = useState('');
const [offset, setOffset] = useState(0);
const [retrying, setRetrying] = useState<string | null>(null);
useEffect(() => {
setOffset(0);
}, [statusFilter, sellerIdFilter]);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params: Record<string, string | number | undefined> = {
limit: LIMIT,
offset,
};
if (statusFilter && statusFilter !== 'all') params.status = statusFilter;
if (sellerIdFilter.trim()) params.seller_id = sellerIdFilter.trim();
const res = await commerceService.getAdminTransfers(params);
setTransfers(res.transfers);
setTotal(res.total);
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)));
setTransfers([]);
setTotal(0);
} finally {
setLoading(false);
}
}, [offset, statusFilter, sellerIdFilter]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleRetry = async (transferId: string) => {
setRetrying(transferId);
try {
await commerceService.retryAdminTransfer(transferId);
addToast({ message: 'Transfer retried successfully', type: 'success' });
fetchData();
} catch (e) {
addToast({
message: e instanceof Error ? e.message : 'Retry failed',
type: 'error',
});
} finally {
setRetrying(null);
}
};
const hasNext = offset + LIMIT < total;
const hasPrev = offset > 0;
if (error) {
return (
<div className="container mx-auto px-4 py-8 max-w-layout-content">
<ErrorDisplay
error={error}
onRetry={fetchData}
title="Failed to load transfers"
context={{ action: 'loading', resource: 'admin transfers' }}
variant="card"
/>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8 max-w-layout-content space-y-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-2xl font-bold text-foreground">Platform Transfers</h1>
<div className="flex flex-wrap items-center gap-2">
<Select
options={[
{ value: 'all', label: 'All statuses' },
{ value: 'completed', label: 'Completed' },
{ value: 'failed', label: 'Failed' },
{ value: 'permanently_failed', label: 'Permanent fail' },
{ value: 'pending', label: 'Pending' },
{ value: 'skipped', label: 'Skipped' },
]}
value={statusFilter}
onChange={(v) => setStatusFilter(Array.isArray(v) ? v[0] ?? 'all' : v ?? 'all')}
placeholder="Status"
className="w-40"
/>
<Input
placeholder="Seller ID"
value={sellerIdFilter}
onChange={(e) => setSellerIdFilter(e.target.value)}
className="w-48"
/>
<Button variant="outline" size="sm" onClick={fetchData} disabled={loading}>
<RefreshCcw className="w-4 h-4 mr-1" />
Refresh
</Button>
</div>
</div>
<Card variant="default" className="overflow-hidden">
{loading ? (
<div className="p-6 space-y-4">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-64 w-full" />
</div>
) : transfers.length === 0 ? (
<div className="p-12 text-center text-muted-foreground">
No transfers found
</div>
) : (
<>
<Table aria-label="Platform transfers">
<TableHeader>
<TableRow>
<TableHead>Seller</TableHead>
<TableHead>Order</TableHead>
<TableHead>Net (EUR)</TableHead>
<TableHead>Fee (EUR)</TableHead>
<TableHead>Status</TableHead>
<TableHead>Retries</TableHead>
<TableHead>Date</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transfers.map((t) => (
<TableRow key={t.id}>
<TableCell className="font-mono text-xs">
{truncateId(t.seller_id ?? t.id)}
</TableCell>
<TableCell className="font-mono text-xs">
{truncateId(t.order_id)}
</TableCell>
<TableCell>
{(t.amount_cents / 100).toFixed(2)}
</TableCell>
<TableCell>
{(t.platform_fee_cents / 100).toFixed(2)}
</TableCell>
<TableCell>
<span
className={`text-xs px-2 py-0.5 rounded ${statusBadgeClass(
t.status
)}`}
>
{t.status}
</span>
</TableCell>
<TableCell>{t.retry_count ?? 0}</TableCell>
<TableCell className="text-muted-foreground text-xs">
{new Date(t.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
{t.status === 'failed' && (
<Button
variant="outline"
size="sm"
onClick={() => handleRetry(t.id)}
disabled={retrying === t.id}
>
{retrying === t.id ? '…' : 'Retry'}
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex items-center justify-between px-4 py-3 border-t border-border">
<span className="text-sm text-muted-foreground">
{offset + 1}{Math.min(offset + LIMIT, total)} of {total}
</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setOffset((o) => Math.max(0, o - LIMIT))}
disabled={!hasPrev}
>
<ChevronLeft className="w-4 h-4" />
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setOffset((o) => o + LIMIT)}
disabled={!hasNext}
>
Next
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
</>
)}
</Card>
</div>
);
}