This commit is contained in:
jackiettran
2025-09-02 16:15:09 -04:00
parent b52104c3fa
commit b59fc07fc3
23 changed files with 1080 additions and 417 deletions

View 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;