payouts
This commit is contained in:
@@ -1,152 +1,141 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const sequelize = require("../config/database");
|
||||
|
||||
const Rental = sequelize.define('Rental', {
|
||||
const Rental = sequelize.define("Rental", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
primaryKey: true,
|
||||
},
|
||||
itemId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Items',
|
||||
key: 'id'
|
||||
}
|
||||
model: "Items",
|
||||
key: "id",
|
||||
},
|
||||
},
|
||||
renterId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
model: "Users",
|
||||
key: "id",
|
||||
},
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
model: "Users",
|
||||
key: "id",
|
||||
},
|
||||
startDate: {
|
||||
},
|
||||
startDateTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
endDate: {
|
||||
endDateTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.STRING
|
||||
allowNull: false,
|
||||
},
|
||||
totalAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
baseRentalAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
platformFee: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
processingFee: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
payoutAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'confirmed', 'active', 'completed', 'cancelled'),
|
||||
defaultValue: 'pending'
|
||||
type: DataTypes.ENUM(
|
||||
"pending",
|
||||
"confirmed",
|
||||
"active",
|
||||
"completed",
|
||||
"cancelled"
|
||||
),
|
||||
defaultValue: "pending",
|
||||
},
|
||||
paymentStatus: {
|
||||
type: DataTypes.ENUM('pending', 'paid', 'refunded'),
|
||||
defaultValue: 'pending'
|
||||
type: DataTypes.ENUM("pending", "paid", "refunded"),
|
||||
defaultValue: "pending",
|
||||
},
|
||||
payoutStatus: {
|
||||
type: DataTypes.ENUM('pending', 'processing', 'completed', 'failed'),
|
||||
defaultValue: 'pending'
|
||||
type: DataTypes.ENUM("pending", "processing", "completed", "failed"),
|
||||
defaultValue: "pending",
|
||||
},
|
||||
payoutProcessedAt: {
|
||||
type: DataTypes.DATE
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
stripeTransferId: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
deliveryMethod: {
|
||||
type: DataTypes.ENUM('pickup', 'delivery'),
|
||||
defaultValue: 'pickup'
|
||||
type: DataTypes.ENUM("pickup", "delivery"),
|
||||
defaultValue: "pickup",
|
||||
},
|
||||
deliveryAddress: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
// Renter's review of the item (existing fields renamed for clarity)
|
||||
itemRating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
itemReview: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
itemReviewSubmittedAt: {
|
||||
type: DataTypes.DATE
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
itemReviewVisible: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
// Owner's review of the renter
|
||||
renterRating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
renterReview: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
renterReviewSubmittedAt: {
|
||||
type: DataTypes.DATE
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
renterReviewVisible: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
// Private messages (always visible to recipient)
|
||||
itemPrivateMessage: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
renterPrivateMessage: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
// Legacy fields for backwards compatibility
|
||||
rating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
},
|
||||
review: {
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Rental;
|
||||
@@ -98,13 +98,12 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
itemId,
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
deliveryMethod,
|
||||
deliveryAddress,
|
||||
notes,
|
||||
paymentStatus,
|
||||
} = req.body;
|
||||
|
||||
const item = await Item.findByPk(itemId);
|
||||
@@ -116,16 +115,57 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
return res.status(400).json({ error: "Item is not available" });
|
||||
}
|
||||
|
||||
let rentalStartDateTime, rentalEndDateTime, baseRentalAmount;
|
||||
|
||||
// New UTC datetime format
|
||||
rentalStartDateTime = new Date(startDateTime);
|
||||
rentalEndDateTime = new Date(endDateTime);
|
||||
|
||||
// Calculate rental duration
|
||||
const diffMs = rentalEndDateTime.getTime() - rentalStartDateTime.getTime();
|
||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Calculate base amount based on duration
|
||||
if (item.pricePerHour && diffHours <= 24) {
|
||||
baseRentalAmount = diffHours * Number(item.pricePerHour);
|
||||
} else if (item.pricePerDay) {
|
||||
baseRentalAmount = diffDays * Number(item.pricePerDay);
|
||||
} else {
|
||||
baseRentalAmount = 0;
|
||||
}
|
||||
|
||||
// Check for overlapping rentals using datetime ranges
|
||||
const overlappingRental = await Rental.findOne({
|
||||
where: {
|
||||
itemId,
|
||||
status: { [Op.in]: ["confirmed", "active"] },
|
||||
[Op.or]: [
|
||||
{
|
||||
startDate: { [Op.between]: [startDate, endDate] },
|
||||
[Op.and]: [
|
||||
{ startDateTime: { [Op.not]: null } },
|
||||
{ endDateTime: { [Op.not]: null } },
|
||||
{
|
||||
[Op.or]: [
|
||||
{
|
||||
startDateTime: {
|
||||
[Op.between]: [rentalStartDateTime, rentalEndDateTime],
|
||||
},
|
||||
},
|
||||
{
|
||||
endDate: { [Op.between]: [startDate, endDate] },
|
||||
endDateTime: {
|
||||
[Op.between]: [rentalStartDateTime, rentalEndDateTime],
|
||||
},
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{ startDateTime: { [Op.lte]: rentalStartDateTime } },
|
||||
{ endDateTime: { [Op.gte]: rentalEndDateTime } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -137,11 +177,6 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
.json({ error: "Item is already booked for these dates" });
|
||||
}
|
||||
|
||||
const rentalDays = Math.ceil(
|
||||
(new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const baseRentalAmount = rentalDays * (item.pricePerDay || 0);
|
||||
|
||||
// Calculate fees using FeeCalculator
|
||||
const fees = FeeCalculator.calculateRentalFees(baseRentalAmount);
|
||||
|
||||
@@ -149,15 +184,14 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
itemId,
|
||||
renterId: req.user.id,
|
||||
ownerId: item.ownerId,
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
startDateTime: rentalStartDateTime,
|
||||
endDateTime: rentalEndDateTime,
|
||||
totalAmount: fees.totalChargedAmount,
|
||||
baseRentalAmount: fees.baseRentalAmount,
|
||||
platformFee: fees.platformFee,
|
||||
processingFee: fees.processingFee,
|
||||
payoutAmount: fees.payoutAmount,
|
||||
paymentStatus: paymentStatus || "pending",
|
||||
deliveryMethod,
|
||||
deliveryAddress,
|
||||
notes,
|
||||
@@ -356,34 +390,6 @@ router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy review endpoint (for backward compatibility)
|
||||
router.post("/:id/review", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { rating, review } = req.body;
|
||||
const rental = await Rental.findByPk(req.params.id);
|
||||
|
||||
if (!rental) {
|
||||
return res.status(404).json({ error: "Rental not found" });
|
||||
}
|
||||
|
||||
if (rental.renterId !== req.user.id) {
|
||||
return res.status(403).json({ error: "Only renters can leave reviews" });
|
||||
}
|
||||
|
||||
if (rental.status !== "completed") {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Can only review completed rentals" });
|
||||
}
|
||||
|
||||
await rental.update({ rating, review });
|
||||
|
||||
res.json(rental);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate fees for rental pricing display
|
||||
router.post("/calculate-fees", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
@@ -406,8 +412,8 @@ router.post("/calculate-fees", authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get payout status for owner's rentals
|
||||
router.get("/payouts/status", authenticateToken, async (req, res) => {
|
||||
// Get earnings status for owner's rentals
|
||||
router.get("/earnings/status", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const ownerRentals = await Rental.findAll({
|
||||
where: {
|
||||
@@ -429,7 +435,7 @@ router.get("/payouts/status", authenticateToken, async (req, res) => {
|
||||
|
||||
res.json(ownerRentals);
|
||||
} catch (error) {
|
||||
console.error("Error getting payout status:", error);
|
||||
console.error("Error getting earnings status:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
const express = require("express");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const { User } = require("../models");
|
||||
const { Rental, Item } = require("../models");
|
||||
const { User, Item } = require("../models");
|
||||
const StripeService = require("../services/stripeService");
|
||||
const router = express.Router();
|
||||
const platformFee = 0.1;
|
||||
|
||||
router.post("/create-checkout-session", async (req, res) => {
|
||||
router.post("/create-checkout-session", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemName, total, return_url, rentalData } = req.body;
|
||||
|
||||
@@ -20,18 +18,37 @@ router.post("/create-checkout-session", async (req, res) => {
|
||||
return res.status(400).json({ error: "No return_url found" });
|
||||
}
|
||||
|
||||
// Validate rental data and user authorization
|
||||
if (rentalData && rentalData.itemId) {
|
||||
const item = await Item.findByPk(rentalData.itemId);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: "Item not found" });
|
||||
}
|
||||
|
||||
if (!item.availability) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Item is not available for rent" });
|
||||
}
|
||||
|
||||
// Check if user is trying to rent their own item
|
||||
if (item.ownerId === req.user.id) {
|
||||
return res.status(400).json({ error: "You cannot rent your own item" });
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare metadata - Stripe metadata keys must be strings
|
||||
const metadata = rentalData
|
||||
? {
|
||||
itemId: rentalData.itemId,
|
||||
startDate: rentalData.startDate,
|
||||
endDate: rentalData.endDate,
|
||||
startTime: rentalData.startTime,
|
||||
endTime: rentalData.endTime,
|
||||
renterId: req.user.id.toString(), // Add authenticated user ID
|
||||
startDateTime: rentalData.startDateTime,
|
||||
endDateTime: rentalData.endDateTime,
|
||||
totalAmount: rentalData.totalAmount.toString(),
|
||||
deliveryMethod: rentalData.deliveryMethod,
|
||||
}
|
||||
: {};
|
||||
: { renterId: req.user.id.toString() };
|
||||
|
||||
const session = await StripeService.createCheckoutSession({
|
||||
item_name: itemName,
|
||||
|
||||
@@ -59,6 +59,8 @@ class PayoutService {
|
||||
ownerId: rental.ownerId,
|
||||
baseAmount: rental.baseRentalAmount.toString(),
|
||||
platformFee: rental.platformFee.toString(),
|
||||
startDateTime: rental.startDateTime.toISOString(),
|
||||
endDateTime: rental.endDateTime.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class StripeService {
|
||||
static async createConnectedAccount({ email, country = "US" }) {
|
||||
try {
|
||||
const account = await stripe.accounts.create({
|
||||
type: "standard",
|
||||
type: "express",
|
||||
email,
|
||||
country,
|
||||
capabilities: {
|
||||
|
||||
@@ -22,6 +22,7 @@ import ItemRequests from './pages/ItemRequests';
|
||||
import ItemRequestDetail from './pages/ItemRequestDetail';
|
||||
import CreateItemRequest from './pages/CreateItemRequest';
|
||||
import MyRequests from './pages/MyRequests';
|
||||
import EarningsDashboard from './pages/EarningsDashboard';
|
||||
import CheckoutReturn from './components/CheckoutReturn';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import './App.css';
|
||||
@@ -123,6 +124,14 @@ function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/earnings"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<EarningsDashboard />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/checkout/return"
|
||||
element={
|
||||
|
||||
@@ -38,12 +38,11 @@ const CheckoutReturn: React.FC = () => {
|
||||
// Convert metadata back to proper types
|
||||
const rentalData = {
|
||||
itemId: metadata.itemId,
|
||||
startDate: metadata.startDate,
|
||||
endDate: metadata.endDate,
|
||||
startTime: metadata.startTime,
|
||||
endTime: metadata.endTime,
|
||||
startDateTime: metadata.startDateTime,
|
||||
endDateTime: metadata.endDateTime,
|
||||
totalAmount: parseFloat(metadata.totalAmount),
|
||||
deliveryMethod: metadata.deliveryMethod,
|
||||
paymentStatus: "paid", // Set since payment already succeeded
|
||||
};
|
||||
|
||||
const response = await rentalAPI.createRental(rentalData);
|
||||
|
||||
76
frontend/src/components/EarningsStatus.tsx
Normal file
76
frontend/src/components/EarningsStatus.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
|
||||
interface EarningsStatusProps {
|
||||
hasStripeAccount: boolean;
|
||||
onSetupClick: () => void;
|
||||
}
|
||||
|
||||
const EarningsStatus: React.FC<EarningsStatusProps> = ({
|
||||
hasStripeAccount,
|
||||
onSetupClick,
|
||||
}) => {
|
||||
// No Stripe account exists
|
||||
if (!hasStripeAccount) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-exclamation-circle text-warning"
|
||||
style={{ fontSize: "2.5rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h6>Earnings Not Set Up</h6>
|
||||
<p className="text-muted small mb-3">
|
||||
Set up earnings to automatically receive payments when rentals are
|
||||
completed.
|
||||
</p>
|
||||
<button className="btn btn-primary" onClick={onSetupClick}>
|
||||
Set Up Earnings
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Account exists and is set up
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-check-circle text-success"
|
||||
style={{ fontSize: "2.5rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h6 className="text-success">Earnings Active</h6>
|
||||
<p className="text-muted small mb-3">
|
||||
Your earnings are set up and working. You'll receive payments
|
||||
automatically.
|
||||
</p>
|
||||
|
||||
<div className="small text-start">
|
||||
<div className="d-flex justify-content-between mb-1">
|
||||
<span>Earnings Enabled:</span>
|
||||
<span className="text-success">
|
||||
<i className="bi bi-check"></i> Yes
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between">
|
||||
<span>Status:</span>
|
||||
<span className="text-success">
|
||||
<i className="bi bi-check"></i> Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => window.open("https://dashboard.stripe.com", "_blank")}
|
||||
>
|
||||
<i className="bi bi-box-arrow-up-right"></i> Stripe Dashboard
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EarningsStatus;
|
||||
@@ -162,6 +162,12 @@ const Navbar: React.FC = () => {
|
||||
Looking For
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/earnings">
|
||||
<i className="bi bi-cash-coin me-2"></i>
|
||||
Earnings
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/messages">
|
||||
<i className="bi bi-envelope me-2"></i>Messages
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI, itemAPI } from '../services/api';
|
||||
import { ItemRequest, Item } from '../types';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { itemRequestAPI, itemAPI } from "../services/api";
|
||||
import { ItemRequest, Item } from "../types";
|
||||
|
||||
interface RequestResponseModalProps {
|
||||
show: boolean;
|
||||
@@ -14,7 +14,7 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
show,
|
||||
onHide,
|
||||
request,
|
||||
onResponseSubmitted
|
||||
onResponseSubmitted,
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -22,13 +22,13 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
const [userItems, setUserItems] = useState<Item[]>([]);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
message: '',
|
||||
offerPricePerHour: '',
|
||||
offerPricePerDay: '',
|
||||
availableStartDate: '',
|
||||
availableEndDate: '',
|
||||
existingItemId: '',
|
||||
contactInfo: ''
|
||||
message: "",
|
||||
offerPricePerHour: "",
|
||||
offerPricePerDay: "",
|
||||
availableStartDate: "",
|
||||
availableEndDate: "",
|
||||
existingItemId: "",
|
||||
contactInfo: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,26 +43,30 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
const response = await itemAPI.getItems({ owner: user?.id });
|
||||
setUserItems(response.data.items || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user items:', err);
|
||||
console.error("Failed to fetch user items:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
message: '',
|
||||
offerPricePerHour: '',
|
||||
offerPricePerDay: '',
|
||||
availableStartDate: '',
|
||||
availableEndDate: '',
|
||||
existingItemId: '',
|
||||
contactInfo: ''
|
||||
message: "",
|
||||
offerPricePerHour: "",
|
||||
offerPricePerDay: "",
|
||||
availableStartDate: "",
|
||||
availableEndDate: "",
|
||||
existingItemId: "",
|
||||
contactInfo: "",
|
||||
});
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@@ -75,18 +79,22 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
try {
|
||||
const responseData = {
|
||||
...formData,
|
||||
offerPricePerHour: formData.offerPricePerHour ? parseFloat(formData.offerPricePerHour) : null,
|
||||
offerPricePerDay: formData.offerPricePerDay ? parseFloat(formData.offerPricePerDay) : null,
|
||||
offerPricePerHour: formData.offerPricePerHour
|
||||
? parseFloat(formData.offerPricePerHour)
|
||||
: null,
|
||||
offerPricePerDay: formData.offerPricePerDay
|
||||
? parseFloat(formData.offerPricePerDay)
|
||||
: null,
|
||||
existingItemId: formData.existingItemId || null,
|
||||
availableStartDate: formData.availableStartDate || null,
|
||||
availableEndDate: formData.availableEndDate || null
|
||||
availableEndDate: formData.availableEndDate || null,
|
||||
};
|
||||
|
||||
await itemRequestAPI.respondToRequest(request.id, responseData);
|
||||
onResponseSubmitted();
|
||||
onHide();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to submit response');
|
||||
setError(err.response?.data?.error || "Failed to submit response");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -95,12 +103,20 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
if (!request) return null;
|
||||
|
||||
return (
|
||||
<div className={`modal fade ${show ? 'show d-block' : ''}`} tabIndex={-1} style={{ backgroundColor: show ? 'rgba(0,0,0,0.5)' : 'transparent' }}>
|
||||
<div
|
||||
className={`modal fade ${show ? "show d-block" : ""}`}
|
||||
tabIndex={-1}
|
||||
style={{ backgroundColor: show ? "rgba(0,0,0,0.5)" : "transparent" }}
|
||||
>
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Respond to Request</h5>
|
||||
<button type="button" className="btn-close" onClick={onHide}></button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={onHide}
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
@@ -117,7 +133,9 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="message" className="form-label">Your Message *</label>
|
||||
<label htmlFor="message" className="form-label">
|
||||
Your Message *
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="message"
|
||||
@@ -132,7 +150,9 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
|
||||
{userItems.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<label htmlFor="existingItemId" className="form-label">Do you have an existing listing for this item?</label>
|
||||
<label htmlFor="existingItemId" className="form-label">
|
||||
Do you have an existing listing for this item?
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="existingItemId"
|
||||
@@ -148,14 +168,17 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
))}
|
||||
</select>
|
||||
<div className="form-text">
|
||||
If you have an existing listing that matches this request, select it here.
|
||||
If you have an existing listing that matches this request,
|
||||
select it here.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="offerPricePerDay" className="form-label">Your Price per Day</label>
|
||||
<label htmlFor="offerPricePerDay" className="form-label">
|
||||
Your Price per Day
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
@@ -172,7 +195,9 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="offerPricePerHour" className="form-label">Your Price per Hour</label>
|
||||
<label htmlFor="offerPricePerHour" className="form-label">
|
||||
Your Price per Hour
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
@@ -192,7 +217,9 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="availableStartDate" className="form-label">Available From</label>
|
||||
<label htmlFor="availableStartDate" className="form-label">
|
||||
Available From
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -200,11 +227,13 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
name="availableStartDate"
|
||||
value={formData.availableStartDate}
|
||||
onChange={handleChange}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
min={new Date().toLocaleDateString()}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="availableEndDate" className="form-label">Available Until</label>
|
||||
<label htmlFor="availableEndDate" className="form-label">
|
||||
Available Until
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -212,13 +241,18 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
name="availableEndDate"
|
||||
value={formData.availableEndDate}
|
||||
onChange={handleChange}
|
||||
min={formData.availableStartDate || new Date().toISOString().split('T')[0]}
|
||||
min={
|
||||
formData.availableStartDate ||
|
||||
new Date().toLocaleDateString()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="contactInfo" className="form-label">Contact Information</label>
|
||||
<label htmlFor="contactInfo" className="form-label">
|
||||
Contact Information
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -236,7 +270,11 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onHide}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={onHide}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@@ -245,7 +283,7 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !formData.message.trim()}
|
||||
>
|
||||
{loading ? 'Submitting...' : 'Submit Response'}
|
||||
{loading ? "Submitting..." : "Submit Response"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,23 +17,9 @@ const ReviewDetailsModal: React.FC<ReviewDetailsModalProps> = ({
|
||||
}) => {
|
||||
if (!show) return null;
|
||||
|
||||
const formatDateTime = (dateString: string, timeString?: string) => {
|
||||
const formatDateTime = (dateString: string) => {
|
||||
const date = new Date(dateString).toLocaleDateString();
|
||||
const formattedTime = timeString
|
||||
? (() => {
|
||||
try {
|
||||
const [hour, minute] = timeString.split(":");
|
||||
const hourNum = parseInt(hour);
|
||||
const hour12 =
|
||||
hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
|
||||
const period = hourNum < 12 ? "AM" : "PM";
|
||||
return `${hour12}:${minute} ${period}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
})()
|
||||
: "";
|
||||
return formattedTime ? `${date} at ${formattedTime}` : date;
|
||||
return date;
|
||||
};
|
||||
|
||||
const isRenter = userType === "renter";
|
||||
@@ -60,8 +46,8 @@ const ReviewDetailsModal: React.FC<ReviewDetailsModalProps> = ({
|
||||
<div className="mb-4 text-center">
|
||||
<h6 className="mb-1">{rental.item.name}</h6>
|
||||
<small className="text-muted">
|
||||
{formatDateTime(rental.startDate, rental.startTime)} to{" "}
|
||||
{formatDateTime(rental.endDate, rental.endTime)}
|
||||
{formatDateTime(rental.startDateTime)} to{" "}
|
||||
{formatDateTime(rental.endDateTime)}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,9 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
|
||||
if (response.data.reviewVisible) {
|
||||
setSuccessMessage("Review published successfully!");
|
||||
} else {
|
||||
setSuccessMessage("Review submitted! It will be published when both parties have reviewed or after 10 minutes.");
|
||||
setSuccessMessage(
|
||||
"Review submitted! It will be published when both parties have reviewed or after 10 minutes."
|
||||
);
|
||||
}
|
||||
setShowSuccessModal(true);
|
||||
} catch (err: any) {
|
||||
@@ -114,8 +116,8 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
|
||||
</h6>
|
||||
<p className="mb-1 text-muted small">{rental.item.name}</p>
|
||||
<small className="text-muted">
|
||||
{new Date(rental.startDate).toLocaleDateString()} to{" "}
|
||||
{new Date(rental.endDate).toLocaleDateString()}
|
||||
{new Date(rental.startDateTime).toLocaleDateString()} to{" "}
|
||||
{new Date(rental.endDateTime).toLocaleDateString()}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,9 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
|
||||
if (response.data.reviewVisible) {
|
||||
setSuccessMessage("Review published successfully!");
|
||||
} else {
|
||||
setSuccessMessage("Review submitted! It will be published when both parties have reviewed or after 10 minutes.");
|
||||
setSuccessMessage(
|
||||
"Review submitted! It will be published when both parties have reviewed or after 10 minutes."
|
||||
);
|
||||
}
|
||||
setShowSuccessModal(true);
|
||||
} catch (err: any) {
|
||||
@@ -114,8 +116,8 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
|
||||
</h6>
|
||||
<p className="mb-1 text-muted small">{rental.item.name}</p>
|
||||
<small className="text-muted">
|
||||
{new Date(rental.startDate).toLocaleDateString()} to{" "}
|
||||
{new Date(rental.endDate).toLocaleDateString()}
|
||||
{new Date(rental.startDateTime).toLocaleDateString()} to{" "}
|
||||
{new Date(rental.endDateTime).toLocaleDateString()}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
211
frontend/src/components/StripeConnectOnboarding.tsx
Normal file
211
frontend/src/components/StripeConnectOnboarding.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React, { useState } from "react";
|
||||
import { stripeAPI } from "../services/api";
|
||||
|
||||
interface StripeConnectOnboardingProps {
|
||||
onComplete: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const StripeConnectOnboarding: React.FC<StripeConnectOnboardingProps> = ({
|
||||
onComplete,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [step, setStep] = useState<"start" | "creating" | "redirecting">(
|
||||
"start"
|
||||
);
|
||||
|
||||
const createStripeAccount = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setStep("creating");
|
||||
|
||||
try {
|
||||
// First, create the Stripe Connected Account
|
||||
const accountResponse = await stripeAPI.createConnectedAccount();
|
||||
|
||||
setStep("redirecting");
|
||||
|
||||
// Generate onboarding link
|
||||
const refreshUrl = `${window.location.origin}/earnings?refresh=true`;
|
||||
const returnUrl = `${window.location.origin}/earnings?setup=complete`;
|
||||
|
||||
const linkResponse = await stripeAPI.createAccountLink({
|
||||
refreshUrl,
|
||||
returnUrl,
|
||||
});
|
||||
|
||||
const { url } = linkResponse.data;
|
||||
|
||||
// Redirect to Stripe onboarding
|
||||
window.location.href = url;
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || err.message || "Failed to set up earnings"
|
||||
);
|
||||
setStep("start");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartSetup = () => {
|
||||
createStripeAccount();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal fade show d-block"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Set Up Earnings</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{step === "start" && (
|
||||
<>
|
||||
<div className="text-center mb-4">
|
||||
<div className="text-primary mb-3">
|
||||
<i
|
||||
className="bi bi-cash-coin"
|
||||
style={{ fontSize: "3rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h4>Start Receiving Earnings</h4>
|
||||
<p className="text-muted">
|
||||
Set up your earnings account to automatically receive
|
||||
payments
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="row text-center mb-4">
|
||||
<div className="col-md-4">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-shield-check text-success"
|
||||
style={{ fontSize: "2rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h6>Secure</h6>
|
||||
<small className="text-muted">
|
||||
Powered by Stripe, trusted by millions
|
||||
</small>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-clock text-primary"
|
||||
style={{ fontSize: "2rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h6>Automatic</h6>
|
||||
<small className="text-muted">
|
||||
Earnings are processed automatically
|
||||
</small>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-bank text-info"
|
||||
style={{ fontSize: "2rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h6>Direct Deposit</h6>
|
||||
<small className="text-muted">
|
||||
Funds go directly to your bank
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="alert alert-info">
|
||||
<h6>
|
||||
<i className="bi bi-info-circle"></i> What to expect:
|
||||
</h6>
|
||||
<ul className="mb-0">
|
||||
<li>
|
||||
You'll be redirected to Stripe to verify your identity
|
||||
</li>
|
||||
<li>Provide bank account details for deposits</li>
|
||||
<li>The setup process takes about 5 minutes</li>
|
||||
<li>Start earning immediately after setup</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger">
|
||||
<i className="bi bi-exclamation-triangle"></i> {error}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === "creating" && (
|
||||
<div className="text-center">
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>Creating your earnings account...</h5>
|
||||
<p className="text-muted">
|
||||
Please wait while we set up your account
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "redirecting" && (
|
||||
<div className="text-center">
|
||||
<div className="spinner-border text-success mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>Redirecting to Stripe...</h5>
|
||||
<p className="text-muted">
|
||||
You'll be redirected to complete the setup process. This may
|
||||
take a moment.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{step === "start" && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleStartSetup}
|
||||
disabled={loading}
|
||||
>
|
||||
Set Up Earnings
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(step === "creating" || step === "redirecting") && (
|
||||
<div className="w-100 text-center">
|
||||
<small className="text-muted">
|
||||
Please don't close this window...
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StripeConnectOnboarding;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { itemRequestAPI } from '../services/api';
|
||||
import AddressAutocomplete from '../components/AddressAutocomplete';
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { itemRequestAPI } from "../services/api";
|
||||
import AddressAutocomplete from "../components/AddressAutocomplete";
|
||||
|
||||
const CreateItemRequest: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -11,35 +11,39 @@ const CreateItemRequest: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: '',
|
||||
country: 'US',
|
||||
title: "",
|
||||
description: "",
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "US",
|
||||
latitude: undefined as number | undefined,
|
||||
longitude: undefined as number | undefined,
|
||||
maxPricePerHour: '',
|
||||
maxPricePerDay: '',
|
||||
preferredStartDate: '',
|
||||
preferredEndDate: '',
|
||||
isFlexibleDates: true
|
||||
maxPricePerHour: "",
|
||||
maxPricePerDay: "",
|
||||
preferredStartDate: "",
|
||||
preferredEndDate: "",
|
||||
isFlexibleDates: true,
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
>
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
if (type === 'checkbox') {
|
||||
if (type === "checkbox") {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
setFormData(prev => ({ ...prev, [name]: checked }));
|
||||
setFormData((prev) => ({ ...prev, [name]: checked }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddressChange = (value: string, lat?: number, lon?: number) => {
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address1: value,
|
||||
latitude: lat,
|
||||
@@ -47,7 +51,7 @@ const CreateItemRequest: React.FC = () => {
|
||||
city: prev.city,
|
||||
state: prev.state,
|
||||
zipCode: prev.zipCode,
|
||||
country: prev.country
|
||||
country: prev.country,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -61,16 +65,20 @@ const CreateItemRequest: React.FC = () => {
|
||||
try {
|
||||
const requestData = {
|
||||
...formData,
|
||||
maxPricePerHour: formData.maxPricePerHour ? parseFloat(formData.maxPricePerHour) : null,
|
||||
maxPricePerDay: formData.maxPricePerDay ? parseFloat(formData.maxPricePerDay) : null,
|
||||
maxPricePerHour: formData.maxPricePerHour
|
||||
? parseFloat(formData.maxPricePerHour)
|
||||
: null,
|
||||
maxPricePerDay: formData.maxPricePerDay
|
||||
? parseFloat(formData.maxPricePerDay)
|
||||
: null,
|
||||
preferredStartDate: formData.preferredStartDate || null,
|
||||
preferredEndDate: formData.preferredEndDate || null
|
||||
preferredEndDate: formData.preferredEndDate || null,
|
||||
};
|
||||
|
||||
await itemRequestAPI.createItemRequest(requestData);
|
||||
navigate('/my-requests');
|
||||
navigate("/my-requests");
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to create item request');
|
||||
setError(err.response?.data?.error || "Failed to create item request");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -92,7 +100,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2 className="mb-0">Request an Item</h2>
|
||||
<p className="text-muted mb-0">Can't find what you need? Request it and let others know!</p>
|
||||
<p className="text-muted mb-0">
|
||||
Can't find what you need? Request it and let others know!
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{error && (
|
||||
@@ -103,7 +113,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="title" className="form-label">What are you looking for? *</label>
|
||||
<label htmlFor="title" className="form-label">
|
||||
What are you looking for? *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -117,7 +129,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="description" className="form-label">Description *</label>
|
||||
<label htmlFor="description" className="form-label">
|
||||
Description *
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="description"
|
||||
@@ -132,7 +146,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="maxPricePerDay" className="form-label">Max Price per Day</label>
|
||||
<label htmlFor="maxPricePerDay" className="form-label">
|
||||
Max Price per Day
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
@@ -149,7 +165,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="maxPricePerHour" className="form-label">Max Price per Hour</label>
|
||||
<label htmlFor="maxPricePerHour" className="form-label">
|
||||
Max Price per Hour
|
||||
</label>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
@@ -178,7 +196,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="address2" className="form-label">Apartment, suite, etc.</label>
|
||||
<label htmlFor="address2" className="form-label">
|
||||
Apartment, suite, etc.
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -190,7 +210,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="city" className="form-label">City</label>
|
||||
<label htmlFor="city" className="form-label">
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -205,7 +227,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="state" className="form-label">State</label>
|
||||
<label htmlFor="state" className="form-label">
|
||||
State
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -217,7 +241,9 @@ const CreateItemRequest: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="zipCode" className="form-label">ZIP Code</label>
|
||||
<label htmlFor="zipCode" className="form-label">
|
||||
ZIP Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -240,7 +266,10 @@ const CreateItemRequest: React.FC = () => {
|
||||
checked={formData.isFlexibleDates}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="isFlexibleDates">
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="isFlexibleDates"
|
||||
>
|
||||
I'm flexible with dates
|
||||
</label>
|
||||
</div>
|
||||
@@ -249,7 +278,12 @@ const CreateItemRequest: React.FC = () => {
|
||||
{!formData.isFlexibleDates && (
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="preferredStartDate" className="form-label">Preferred Start Date</label>
|
||||
<label
|
||||
htmlFor="preferredStartDate"
|
||||
className="form-label"
|
||||
>
|
||||
Preferred Start Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -257,11 +291,13 @@ const CreateItemRequest: React.FC = () => {
|
||||
name="preferredStartDate"
|
||||
value={formData.preferredStartDate}
|
||||
onChange={handleChange}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
min={new Date().toLocaleDateString()}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="preferredEndDate" className="form-label">Preferred End Date</label>
|
||||
<label htmlFor="preferredEndDate" className="form-label">
|
||||
Preferred End Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -269,7 +305,10 @@ const CreateItemRequest: React.FC = () => {
|
||||
name="preferredEndDate"
|
||||
value={formData.preferredEndDate}
|
||||
onChange={handleChange}
|
||||
min={formData.preferredStartDate || new Date().toISOString().split('T')[0]}
|
||||
min={
|
||||
formData.preferredStartDate ||
|
||||
new Date().toLocaleDateString()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,7 +320,7 @@ const CreateItemRequest: React.FC = () => {
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating Request...' : 'Create Request'}
|
||||
{loading ? "Creating Request..." : "Create Request"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
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;
|
||||
@@ -488,7 +488,7 @@ const ItemDetail: React.FC = () => {
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
min={new Date().toLocaleDateString()}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
@@ -537,7 +537,7 @@ const ItemDetail: React.FC = () => {
|
||||
}
|
||||
min={
|
||||
rentalDates.startDate ||
|
||||
new Date().toISOString().split("T")[0]
|
||||
new Date().toLocaleDateString()
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
|
||||
@@ -22,10 +22,9 @@ const MyListings: React.FC = () => {
|
||||
};
|
||||
|
||||
// Helper function to format date and time together
|
||||
const formatDateTime = (dateString: string, timeString?: string) => {
|
||||
const date = new Date(dateString).toLocaleDateString();
|
||||
const formattedTime = formatTime(timeString);
|
||||
return formattedTime ? `${date} at ${formattedTime}` : date;
|
||||
const formatDateTime = (dateTimeString: string) => {
|
||||
const date = new Date(dateTimeString).toLocaleDateString();
|
||||
return date;
|
||||
};
|
||||
|
||||
const { user } = useAuth();
|
||||
@@ -98,7 +97,6 @@ const MyListings: React.FC = () => {
|
||||
const fetchOwnerRentals = async () => {
|
||||
try {
|
||||
const response = await rentalAPI.getMyListings();
|
||||
console.log("Owner rentals data from backend:", response.data);
|
||||
setOwnerRentals(response.data);
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch owner rentals:", err);
|
||||
@@ -128,7 +126,6 @@ const MyListings: React.FC = () => {
|
||||
|
||||
const handleCompleteClick = async (rental: Rental) => {
|
||||
try {
|
||||
console.log("Marking rental as completed:", rental.id);
|
||||
await rentalAPI.markAsCompleted(rental.id);
|
||||
|
||||
setSelectedRentalForReview(rental);
|
||||
@@ -237,10 +234,8 @@ const MyListings: React.FC = () => {
|
||||
<p className="mb-1 text-dark small">
|
||||
<strong>Period:</strong>
|
||||
<br />
|
||||
{formatDateTime(
|
||||
rental.startDate,
|
||||
rental.startTime
|
||||
)} - {formatDateTime(rental.endDate, rental.endTime)}
|
||||
{formatDateTime(rental.startDateTime)} -{" "}
|
||||
{formatDateTime(rental.endDateTime)}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 text-dark small">
|
||||
|
||||
@@ -21,13 +21,6 @@ const MyRentals: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to format date and time together
|
||||
const formatDateTime = (dateString: string, timeString?: string) => {
|
||||
const date = new Date(dateString).toLocaleDateString();
|
||||
const formattedTime = formatTime(timeString);
|
||||
return formattedTime ? `${date} at ${formattedTime}` : date;
|
||||
};
|
||||
|
||||
const { user } = useAuth();
|
||||
const [rentals, setRentals] = useState<Rental[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -171,19 +164,16 @@ const MyRentals: React.FC = () => {
|
||||
{rental.status.charAt(0).toUpperCase() +
|
||||
rental.status.slice(1)}
|
||||
</span>
|
||||
{rental.paymentStatus === "paid" && (
|
||||
<span className="badge bg-success ms-2">Paid</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Rental Period:</strong>
|
||||
<br />
|
||||
<strong>Start:</strong>{" "}
|
||||
{formatDateTime(rental.startDate, rental.startTime)}
|
||||
{new Date(rental.startDateTime).toLocaleString()}
|
||||
<br />
|
||||
<strong>End:</strong>{" "}
|
||||
{formatDateTime(rental.endDate, rental.endTime)}
|
||||
{new Date(rental.endDateTime).toLocaleString()}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
|
||||
@@ -176,10 +176,9 @@ const Profile: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string, timeString?: string) => {
|
||||
const date = new Date(dateString).toLocaleDateString();
|
||||
const formattedTime = formatTime(timeString);
|
||||
return formattedTime ? `${date} at ${formattedTime}` : date;
|
||||
const formatDateTime = (dateTimeString: string) => {
|
||||
const date = new Date(dateTimeString).toLocaleDateString();
|
||||
return date;
|
||||
};
|
||||
|
||||
const fetchRentalHistory = async () => {
|
||||
@@ -853,16 +852,10 @@ const Profile: React.FC = () => {
|
||||
<strong>Period:</strong>
|
||||
<br />
|
||||
<strong>Start:</strong>{" "}
|
||||
{formatDateTime(
|
||||
rental.startDate,
|
||||
rental.startTime
|
||||
)}
|
||||
{formatDateTime(rental.startDateTime)}
|
||||
<br />
|
||||
<strong>End:</strong>{" "}
|
||||
{formatDateTime(
|
||||
rental.endDate,
|
||||
rental.endTime
|
||||
)}
|
||||
{formatDateTime(rental.endDateTime)}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 small">
|
||||
@@ -994,15 +987,8 @@ const Profile: React.FC = () => {
|
||||
<p className="mb-1 small">
|
||||
<strong>Period:</strong>
|
||||
<br />
|
||||
{formatDateTime(
|
||||
rental.startDate,
|
||||
rental.startTime
|
||||
)}{" "}
|
||||
-{" "}
|
||||
{formatDateTime(
|
||||
rental.endDate,
|
||||
rental.endTime
|
||||
)}
|
||||
{formatDateTime(rental.startDateTime)} -{" "}
|
||||
{formatDateTime(rental.endDateTime)}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 small">
|
||||
|
||||
@@ -28,9 +28,24 @@ const RentItem: React.FC = () => {
|
||||
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
|
||||
const convertToUTC = (dateString: string, timeString: string): string => {
|
||||
if (!dateString || !timeString) {
|
||||
throw new Error("Date and time are required");
|
||||
}
|
||||
|
||||
// Create date in user's local timezone
|
||||
const localDateTime = new Date(`${dateString}T${timeString}`);
|
||||
|
||||
// Return UTC ISO string
|
||||
return localDateTime.toISOString();
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return "";
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
// Use safe date parsing to avoid timezone issues
|
||||
const [year, month, day] = dateString.split("-");
|
||||
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
const formatTime = (timeString: string) => {
|
||||
@@ -167,10 +182,14 @@ const RentItem: React.FC = () => {
|
||||
itemName={item.name}
|
||||
rentalData={{
|
||||
itemId: item.id,
|
||||
startDate: manualSelection.startDate,
|
||||
endDate: manualSelection.endDate,
|
||||
startTime: manualSelection.startTime,
|
||||
endTime: manualSelection.endTime,
|
||||
startDateTime: convertToUTC(
|
||||
manualSelection.startDate,
|
||||
manualSelection.startTime
|
||||
),
|
||||
endDateTime: convertToUTC(
|
||||
manualSelection.endDate,
|
||||
manualSelection.endTime
|
||||
),
|
||||
totalAmount: totalCost,
|
||||
deliveryMethod: "pickup",
|
||||
}}
|
||||
|
||||
@@ -84,7 +84,6 @@ export const rentalAPI = {
|
||||
api.post(`/rentals/${id}/review-renter`, data),
|
||||
reviewItem: (id: string, data: any) =>
|
||||
api.post(`/rentals/${id}/review-item`, data),
|
||||
addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data), // Legacy
|
||||
};
|
||||
|
||||
export const messageAPI = {
|
||||
@@ -119,14 +118,10 @@ export const stripeAPI = {
|
||||
}) => api.post("/stripe/create-checkout-session", data),
|
||||
getCheckoutSession: (sessionId: string) =>
|
||||
api.get(`/stripe/checkout-session/${sessionId}`),
|
||||
// createConnectedAccount: () =>
|
||||
// api.post("/stripe/accounts"),
|
||||
// createAccountLink: (data: { refreshUrl: string; returnUrl: string }) =>
|
||||
// api.post("/stripe/account-links", data),
|
||||
// getAccountStatus: () =>
|
||||
// api.get("/stripe/account-status"),
|
||||
// createPaymentIntent: (data: { rentalId: string; amount: number }) =>
|
||||
// api.post("/stripe/payment-intents", data),
|
||||
createConnectedAccount: () => api.post("/stripe/accounts"),
|
||||
createAccountLink: (data: { refreshUrl: string; returnUrl: string }) =>
|
||||
api.post("/stripe/account-links", data),
|
||||
getAccountStatus: () => api.get("/stripe/account-status"),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface User {
|
||||
country?: string;
|
||||
profileImage?: string;
|
||||
isVerified: boolean;
|
||||
stripeConnectedAccountId?: string;
|
||||
addresses?: Address[];
|
||||
}
|
||||
|
||||
@@ -101,13 +102,20 @@ export interface Rental {
|
||||
itemId: string;
|
||||
renterId: string;
|
||||
ownerId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
startDateTime: string;
|
||||
endDateTime: string;
|
||||
totalAmount: number;
|
||||
// Fee tracking fields
|
||||
baseRentalAmount?: number;
|
||||
platformFee?: number;
|
||||
processingFee?: number;
|
||||
payoutAmount?: number;
|
||||
status: "pending" | "confirmed" | "active" | "completed" | "cancelled";
|
||||
paymentStatus: "pending" | "paid" | "refunded";
|
||||
// Payout status tracking
|
||||
payoutStatus?: "pending" | "processing" | "completed" | "failed";
|
||||
payoutProcessedAt?: string;
|
||||
stripeTransferId?: string;
|
||||
deliveryMethod: "pickup" | "delivery";
|
||||
deliveryAddress?: string;
|
||||
notes?: string;
|
||||
|
||||
Reference in New Issue
Block a user