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:
parent
38530b5a52
commit
849c0e6cb8
2 changed files with 143 additions and 0 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 ?? [];
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue