feat(v0.12.0): F254-F255 frontend marketplace payout and balance UI

- Add seller balance/payout API methods to marketplaceService
- Add seller stats API methods (evolution, top products, sales)
- Add marketplace balance card to SellerDashboardView
- Add manual payout request button (min $100)
- Display auto-payout threshold info ($50 weekly)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
senke 2026-03-10 18:52:37 +01:00
parent 38530b5a52
commit 849c0e6cb8
2 changed files with 143 additions and 0 deletions

View file

@ -56,6 +56,15 @@ export const SellerDashboardView: React.FC<SellerDashboardProps> = ({
const [balance, setBalance] = useState<{ connected: boolean; available: number; pending: number } | null>(null);
const [stripeConnectAvailable, setStripeConnectAvailable] = useState<boolean | null>(null);
const [transfers, setTransfers] = useState<SellerTransfer[]>([]);
// v0.12.0 F254: Marketplace balance
const [marketplaceBalance, setMarketplaceBalance] = useState<{
available_cents: number;
pending_cents: number;
total_earned_cents: number;
total_paid_out_cents: number;
currency: string;
} | null>(null);
const [payoutLoading, setPayoutLoading] = useState(false);
const fetchData = useCallback(async () => {
setLoading(true);
@ -91,6 +100,13 @@ export const SellerDashboardView: React.FC<SellerDashboardProps> = ({
} catch {
setTransfers([]);
}
// v0.12.0 F254: Fetch marketplace balance
try {
const mktBalance = await marketplaceService.getSellerBalance();
setMarketplaceBalance(mktBalance);
} catch {
setMarketplaceBalance(null);
}
} catch (e) {
logger.error('Error loading seller dashboard data', {
error: e instanceof Error ? e.message : String(e),
@ -106,6 +122,20 @@ export const SellerDashboardView: React.FC<SellerDashboardProps> = ({
fetchData();
}, [fetchData]);
const handleRequestPayout = useCallback(async () => {
setPayoutLoading(true);
try {
await marketplaceService.requestPayout();
addToast('Payout requested successfully', 'success');
fetchData();
} catch (err: unknown) {
const msg = (err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error?.message || 'Failed to request payout';
addToast(msg, 'error');
} finally {
setPayoutLoading(false);
}
}, [addToast, fetchData]);
const handleConnectPayments = useCallback(async () => {
try {
const { onboarding_url } = await commerceService.connectStripeOnboard();
@ -292,6 +322,40 @@ export const SellerDashboardView: React.FC<SellerDashboardProps> = ({
</Card>
)}
{/* v0.12.0 F254: Marketplace Balance Card */}
{marketplaceBalance && (
<Card variant="default" className="p-6 relative overflow-hidden group">
<div className="absolute right-0 top-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<DollarSign className="w-16 h-16 text-success" />
</div>
<div className="text-muted-foreground text-xs font-bold uppercase mb-1">
Marketplace Balance
</div>
<div className="text-3xl font-mono font-bold text-foreground mb-1">
{(marketplaceBalance.available_cents / 100).toFixed(2)}
</div>
<div className="text-xs text-muted-foreground mb-3">
Pending: {(marketplaceBalance.pending_cents / 100).toFixed(2)} · Total earned: {(marketplaceBalance.total_earned_cents / 100).toFixed(2)}
</div>
{marketplaceBalance.available_cents >= 10000 && (
<Button
variant="outline"
size="sm"
onClick={handleRequestPayout}
disabled={payoutLoading}
>
{payoutLoading ? <Loader2 className="w-3 h-3 animate-spin mr-1" /> : null}
Request Payout
</Button>
)}
{marketplaceBalance.available_cents > 0 && marketplaceBalance.available_cents < 10000 && (
<div className="text-xs text-muted-foreground">
Min. 100.00 for manual payout · Auto-payout at 50.00 weekly
</div>
)}
</Card>
)}
<Card variant="default" className="p-6 relative overflow-hidden group">
<div className="absolute right-0 top-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<DollarSign className="w-16 h-16 text-warning" />

View file

@ -269,4 +269,83 @@ export const marketplaceService = {
}>(`/commerce/promo/${encodeURIComponent(code.trim())}`);
return response.data;
},
// v0.12.0 F254: Seller balance and payouts
getSellerBalance: async () => {
const response = await apiClient.get<{
available_cents: number;
pending_cents: number;
total_earned_cents: number;
total_paid_out_cents: number;
currency: string;
}>('/sell/marketplace-balance');
return response.data;
},
getSellerPayouts: async (limit = 20, offset = 0) => {
const response = await apiClient.get<{
payouts: Array<{
id: string;
amount_cents: number;
currency: string;
status: string;
payout_method: string;
scheduled_at: string;
processed_at?: string;
created_at: string;
}>;
}>('/sell/payouts', { params: { limit, offset } });
return response.data?.payouts ?? [];
},
requestPayout: async () => {
const response = await apiClient.post<{
id: string;
amount_cents: number;
currency: string;
status: string;
scheduled_at: string;
}>('/sell/payouts/request');
return response.data;
},
// Seller stats
getSellerStats: async () => {
const response = await apiClient.get<{
revenue: number;
sales_count: number;
}>('/sell/stats');
return response.data;
},
getSellerStatsEvolution: async (period = 'day') => {
const response = await apiClient.get<Array<{
date: string;
revenue: number;
sales_count: number;
}>>('/sell/stats/evolution', { params: { period } });
return response.data ?? [];
},
getSellerTopProducts: async (limit = 10) => {
const response = await apiClient.get<Array<{
product_id: string;
title: string;
revenue: number;
sales_count: number;
}>>('/sell/stats/top-products', { params: { limit } });
return response.data ?? [];
},
getSellerSales: async (limit = 20) => {
const response = await apiClient.get<Array<{
order_id: string;
product_id: string;
product_title: string;
buyer_id: string;
amount: number;
date: string;
}>>('/sell/sales', { params: { limit } });
return response.data ?? [];
},
};