payouts
This commit is contained in:
288
frontend/src/pages/EarningsDashboard.tsx
Normal file
288
frontend/src/pages/EarningsDashboard.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { rentalAPI, userAPI } from "../services/api";
|
||||
import { Rental, User } from "../types";
|
||||
import StripeConnectOnboarding from "../components/StripeConnectOnboarding";
|
||||
import EarningsStatus from "../components/EarningsStatus";
|
||||
|
||||
interface EarningsData {
|
||||
totalEarnings: number;
|
||||
pendingEarnings: number;
|
||||
completedEarnings: number;
|
||||
rentalsWithEarnings: Rental[];
|
||||
}
|
||||
|
||||
const EarningsDashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [earningsData, setEarningsData] = useState<EarningsData | null>(null);
|
||||
const [userProfile, setUserProfile] = useState<User | null>(null);
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserProfile();
|
||||
fetchEarningsData();
|
||||
}, []);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const response = await userAPI.getProfile();
|
||||
setUserProfile(response.data);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user profile:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEarningsData = async () => {
|
||||
try {
|
||||
// Get completed rentals where user is the owner
|
||||
const response = await rentalAPI.getMyListings();
|
||||
const rentals = response.data || [];
|
||||
|
||||
// Filter for completed rentals with earnings data
|
||||
const completedRentals = rentals.filter(
|
||||
(rental: Rental) => rental.status === "completed" && rental.payoutAmount
|
||||
);
|
||||
|
||||
// Calculate earnings - convert string values to numbers
|
||||
const totalEarnings = completedRentals.reduce(
|
||||
(sum: number, rental: Rental) =>
|
||||
sum + parseFloat(rental.payoutAmount?.toString() || "0"),
|
||||
0
|
||||
);
|
||||
|
||||
const pendingEarnings = completedRentals
|
||||
.filter((rental: Rental) => rental.payoutStatus === "pending")
|
||||
.reduce(
|
||||
(sum: number, rental: Rental) =>
|
||||
sum + parseFloat(rental.payoutAmount?.toString() || "0"),
|
||||
0
|
||||
);
|
||||
|
||||
const completedEarnings = completedRentals
|
||||
.filter((rental: Rental) => rental.payoutStatus === "completed")
|
||||
.reduce(
|
||||
(sum: number, rental: Rental) =>
|
||||
sum + parseFloat(rental.payoutAmount?.toString() || "0"),
|
||||
0
|
||||
);
|
||||
|
||||
setEarningsData({
|
||||
totalEarnings,
|
||||
pendingEarnings,
|
||||
completedEarnings,
|
||||
rentalsWithEarnings: completedRentals,
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Failed to fetch earnings data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetupComplete = () => {
|
||||
setShowOnboarding(false);
|
||||
fetchUserProfile(); // Refresh user profile after setup
|
||||
fetchEarningsData();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="text-center">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasStripeAccount = !!userProfile?.stripeConnectedAccountId;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<h1>My Earnings</h1>
|
||||
<p className="text-muted">
|
||||
Manage your rental earnings and payment setup
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
{/* Earnings Overview */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Earnings Overview</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row text-center">
|
||||
<div className="col-md-4">
|
||||
<h3 className="text-primary">
|
||||
${(earningsData?.totalEarnings || 0).toFixed(2)}
|
||||
</h3>
|
||||
<p className="text-muted">Total Earnings</p>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<h3 className="text-warning">
|
||||
${(earningsData?.pendingEarnings || 0).toFixed(2)}
|
||||
</h3>
|
||||
<p className="text-muted">Pending Earnings</p>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<h3 className="text-success">
|
||||
${(earningsData?.completedEarnings || 0).toFixed(2)}
|
||||
</h3>
|
||||
<p className="text-muted">Paid Out</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Earnings History */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Earnings History</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{earningsData?.rentalsWithEarnings.length === 0 ? (
|
||||
<p className="text-muted text-center">
|
||||
No completed rentals yet
|
||||
</p>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Rental Period</th>
|
||||
<th>Total Amount</th>
|
||||
<th>Your Earnings</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{earningsData?.rentalsWithEarnings.map((rental) => (
|
||||
<tr key={rental.id}>
|
||||
<td>
|
||||
<Link to={`/items/${rental.itemId}`}>
|
||||
{rental.item?.name || "Item"}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<small>
|
||||
{new Date(rental.startDateTime).toLocaleString()}
|
||||
<br />
|
||||
to
|
||||
<br />
|
||||
{new Date(rental.endDateTime).toLocaleString()}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
$
|
||||
{parseFloat(
|
||||
rental.totalAmount?.toString() || "0"
|
||||
).toFixed(2)}
|
||||
</td>
|
||||
<td className="text-success">
|
||||
<strong>
|
||||
$
|
||||
{parseFloat(
|
||||
rental.payoutAmount?.toString() || "0"
|
||||
).toFixed(2)}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`badge ${
|
||||
rental.payoutStatus === "completed"
|
||||
? "bg-success"
|
||||
: rental.payoutStatus === "processing"
|
||||
? "bg-warning"
|
||||
: "bg-secondary"
|
||||
}`}
|
||||
>
|
||||
{rental.payoutStatus === "completed"
|
||||
? "Paid"
|
||||
: rental.payoutStatus === "processing"
|
||||
? "Processing"
|
||||
: "Pending"}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="col-md-4">
|
||||
{/* Earnings Setup Status */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Earnings Setup</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<EarningsStatus
|
||||
hasStripeAccount={hasStripeAccount}
|
||||
onSetupClick={() => setShowOnboarding(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Quick Stats</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="d-flex justify-content-between mb-2">
|
||||
<span>Completed Rentals:</span>
|
||||
<strong>{earningsData?.rentalsWithEarnings.length || 0}</strong>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between mb-2">
|
||||
<span>Average Earning:</span>
|
||||
<strong>
|
||||
$
|
||||
{earningsData?.rentalsWithEarnings.length
|
||||
? (
|
||||
(earningsData.totalEarnings || 0) /
|
||||
earningsData.rentalsWithEarnings.length
|
||||
).toFixed(2)
|
||||
: "0.00"}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between">
|
||||
<span>Platform Fee:</span>
|
||||
<strong>20%</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stripe Connect Onboarding Modal */}
|
||||
{showOnboarding && (
|
||||
<StripeConnectOnboarding
|
||||
onComplete={handleSetupComplete}
|
||||
onCancel={() => setShowOnboarding(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EarningsDashboard;
|
||||
Reference in New Issue
Block a user