pricing tiers
This commit is contained in:
@@ -48,6 +48,12 @@ const Item = sequelize.define("Item", {
|
|||||||
pricePerDay: {
|
pricePerDay: {
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
},
|
},
|
||||||
|
pricePerWeek: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
},
|
||||||
|
pricePerMonth: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
},
|
||||||
replacementCost: {
|
replacementCost: {
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ const ItemRequestResponse = sequelize.define('ItemRequestResponse', {
|
|||||||
offerPricePerDay: {
|
offerPricePerDay: {
|
||||||
type: DataTypes.DECIMAL(10, 2)
|
type: DataTypes.DECIMAL(10, 2)
|
||||||
},
|
},
|
||||||
|
offerPricePerWeek: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2)
|
||||||
|
},
|
||||||
|
offerPricePerMonth: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2)
|
||||||
|
},
|
||||||
availableStartDate: {
|
availableStartDate: {
|
||||||
type: DataTypes.DATE
|
type: DataTypes.DATE
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { Op } = require("sequelize");
|
|||||||
const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations
|
const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations
|
||||||
const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth");
|
const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth");
|
||||||
const FeeCalculator = require("../utils/feeCalculator");
|
const FeeCalculator = require("../utils/feeCalculator");
|
||||||
|
const RentalDurationCalculator = require("../utils/rentalDurationCalculator");
|
||||||
const RefundService = require("../services/refundService");
|
const RefundService = require("../services/refundService");
|
||||||
const LateReturnService = require("../services/lateReturnService");
|
const LateReturnService = require("../services/lateReturnService");
|
||||||
const DamageAssessmentService = require("../services/damageAssessmentService");
|
const DamageAssessmentService = require("../services/damageAssessmentService");
|
||||||
@@ -201,19 +202,12 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
|||||||
rentalStartDateTime = new Date(startDateTime);
|
rentalStartDateTime = new Date(startDateTime);
|
||||||
rentalEndDateTime = new Date(endDateTime);
|
rentalEndDateTime = new Date(endDateTime);
|
||||||
|
|
||||||
// Calculate rental duration
|
// Calculate rental cost using duration calculator
|
||||||
const diffMs = rentalEndDateTime.getTime() - rentalStartDateTime.getTime();
|
totalAmount = RentalDurationCalculator.calculateRentalCost(
|
||||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
rentalStartDateTime,
|
||||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
rentalEndDateTime,
|
||||||
|
item
|
||||||
// Calculate base amount based on duration
|
);
|
||||||
if (item.pricePerHour && diffHours <= 24) {
|
|
||||||
totalAmount = diffHours * Number(item.pricePerHour);
|
|
||||||
} else if (item.pricePerDay) {
|
|
||||||
totalAmount = diffDays * Number(item.pricePerDay);
|
|
||||||
} else {
|
|
||||||
totalAmount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for overlapping rentals using datetime ranges
|
// Check for overlapping rentals using datetime ranges
|
||||||
const overlappingRental = await Rental.findOne({
|
const overlappingRental = await Rental.findOne({
|
||||||
@@ -884,6 +878,60 @@ router.post("/calculate-fees", authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Preview rental cost calculation (no rental creation)
|
||||||
|
router.post("/cost-preview", authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { itemId, startDateTime, endDateTime } = req.body;
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (!itemId || !startDateTime || !endDateTime) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "itemId, startDateTime, and endDateTime are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch item
|
||||||
|
const item = await Item.findByPk(itemId);
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({ error: "Item not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse datetimes
|
||||||
|
const rentalStartDateTime = new Date(startDateTime);
|
||||||
|
const rentalEndDateTime = new Date(endDateTime);
|
||||||
|
|
||||||
|
// Validate date range
|
||||||
|
if (rentalEndDateTime <= rentalStartDateTime) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "End date/time must be after start date/time",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate rental cost using duration calculator
|
||||||
|
const totalAmount = RentalDurationCalculator.calculateRentalCost(
|
||||||
|
rentalStartDateTime,
|
||||||
|
rentalEndDateTime,
|
||||||
|
item
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate fees
|
||||||
|
const fees = FeeCalculator.calculateRentalFees(totalAmount);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
baseAmount: totalAmount,
|
||||||
|
fees,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
|
reqLogger.error("Error calculating rental cost preview", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
userId: req.user.id,
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: "Failed to calculate rental cost preview" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get earnings status for owner's rentals
|
// Get earnings status for owner's rentals
|
||||||
router.get("/earnings/status", authenticateToken, async (req, res) => {
|
router.get("/earnings/status", authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,33 +25,27 @@ class LateReturnService {
|
|||||||
|
|
||||||
let lateFee = 0;
|
let lateFee = 0;
|
||||||
let pricingType = "daily";
|
let pricingType = "daily";
|
||||||
|
|
||||||
// Check if item has hourly or daily pricing
|
|
||||||
if (rental.item?.pricePerHour && rental.item.pricePerHour > 0) {
|
|
||||||
// Hourly pricing - charge per hour late
|
|
||||||
lateFee = hoursLate * parseFloat(rental.item.pricePerHour);
|
|
||||||
pricingType = "hourly";
|
|
||||||
} else if (rental.item?.pricePerDay && rental.item.pricePerDay > 0) {
|
|
||||||
// Daily pricing - charge per day late (rounded up)
|
|
||||||
const billableDays = Math.ceil(hoursLate / 24);
|
const billableDays = Math.ceil(hoursLate / 24);
|
||||||
|
|
||||||
|
// Calculate late fees per day, deriving daily rate from available pricing tiers
|
||||||
|
if (rental.item?.pricePerDay && rental.item.pricePerDay > 0) {
|
||||||
|
// Daily pricing - charge per day late
|
||||||
lateFee = billableDays * parseFloat(rental.item.pricePerDay);
|
lateFee = billableDays * parseFloat(rental.item.pricePerDay);
|
||||||
pricingType = "daily";
|
} else if (rental.item?.pricePerWeek && rental.item.pricePerWeek > 0) {
|
||||||
|
// Weekly pricing - derive daily rate and charge per day late
|
||||||
|
const dailyRate = parseFloat(rental.item.pricePerWeek) / 7;
|
||||||
|
lateFee = billableDays * dailyRate;
|
||||||
|
} else if (rental.item?.pricePerMonth && rental.item.pricePerMonth > 0) {
|
||||||
|
// Monthly pricing - derive daily rate and charge per day late
|
||||||
|
const dailyRate = parseFloat(rental.item.pricePerMonth) / 30;
|
||||||
|
lateFee = billableDays * dailyRate;
|
||||||
|
} else if (rental.item?.pricePerHour && rental.item.pricePerHour > 0) {
|
||||||
|
// Hourly pricing - derive daily rate and charge per day late
|
||||||
|
const dailyRate = parseFloat(rental.item.pricePerHour) * 24;
|
||||||
|
lateFee = billableDays * dailyRate;
|
||||||
} else {
|
} else {
|
||||||
// Free borrows: determine pricing type based on rental duration
|
// Free borrows - charge $10 per day late
|
||||||
const rentalStart = new Date(rental.startDateTime);
|
|
||||||
const rentalEnd = new Date(rental.endDateTime);
|
|
||||||
const rentalDurationHours = (rentalEnd - rentalStart) / (1000 * 60 * 60);
|
|
||||||
|
|
||||||
if (rentalDurationHours <= 24) {
|
|
||||||
// Hourly rental - charge $10 per hour late
|
|
||||||
lateFee = hoursLate * 10.0;
|
|
||||||
pricingType = "hourly";
|
|
||||||
} else {
|
|
||||||
// Daily rental - charge $10 per day late
|
|
||||||
const billableDays = Math.ceil(hoursLate / 24);
|
|
||||||
lateFee = billableDays * 10.0;
|
lateFee = billableDays * 10.0;
|
||||||
pricingType = "daily";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
62
backend/utils/rentalDurationCalculator.js
Normal file
62
backend/utils/rentalDurationCalculator.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
class RentalDurationCalculator {
|
||||||
|
/**
|
||||||
|
* Calculate rental cost based on duration and item pricing tiers
|
||||||
|
* @param {Date|string} startDateTime - Rental start date/time
|
||||||
|
* @param {Date|string} endDateTime - Rental end date/time
|
||||||
|
* @param {Object} item - Item object with pricing information
|
||||||
|
* @returns {number} Total rental cost
|
||||||
|
*/
|
||||||
|
static calculateRentalCost(startDateTime, endDateTime, item) {
|
||||||
|
const rentalStartDateTime = new Date(startDateTime);
|
||||||
|
const 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));
|
||||||
|
const diffWeeks = Math.ceil(diffDays / 7);
|
||||||
|
|
||||||
|
// Calculate difference in calendar months
|
||||||
|
let diffMonths = (rentalEndDateTime.getFullYear() - rentalStartDateTime.getFullYear()) * 12;
|
||||||
|
diffMonths += rentalEndDateTime.getMonth() - rentalStartDateTime.getMonth();
|
||||||
|
|
||||||
|
// Add 1 if we're past the start day/time in the end month
|
||||||
|
if (rentalEndDateTime.getDate() > rentalStartDateTime.getDate()) {
|
||||||
|
diffMonths += 1;
|
||||||
|
} else if (rentalEndDateTime.getDate() === rentalStartDateTime.getDate() &&
|
||||||
|
rentalEndDateTime.getTime() > rentalStartDateTime.getTime()) {
|
||||||
|
diffMonths += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
diffMonths = Math.max(1, diffMonths);
|
||||||
|
|
||||||
|
// Calculate base amount based on duration (tiered pricing)
|
||||||
|
let totalAmount;
|
||||||
|
|
||||||
|
if (item.pricePerHour && diffHours <= 24) {
|
||||||
|
// Use hourly rate for rentals <= 24 hours
|
||||||
|
totalAmount = diffHours * Number(item.pricePerHour);
|
||||||
|
} else if (diffDays <= 7 && item.pricePerDay) {
|
||||||
|
// Use daily rate for rentals <= 7 days
|
||||||
|
totalAmount = diffDays * Number(item.pricePerDay);
|
||||||
|
} else if (diffMonths <= 1 && item.pricePerWeek) {
|
||||||
|
// Use weekly rate for rentals <= 1 calendar month
|
||||||
|
totalAmount = diffWeeks * Number(item.pricePerWeek);
|
||||||
|
} else if (diffMonths > 1 && item.pricePerMonth) {
|
||||||
|
// Use monthly rate for rentals > 1 calendar month
|
||||||
|
totalAmount = diffMonths * Number(item.pricePerMonth);
|
||||||
|
} else if (item.pricePerWeek) {
|
||||||
|
// Fallback to weekly rate if monthly not available
|
||||||
|
totalAmount = diffWeeks * Number(item.pricePerWeek);
|
||||||
|
} else if (item.pricePerDay) {
|
||||||
|
// Fallback to daily rate
|
||||||
|
totalAmount = diffDays * Number(item.pricePerDay);
|
||||||
|
} else {
|
||||||
|
totalAmount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RentalDurationCalculator;
|
||||||
@@ -14,16 +14,28 @@ const ItemCard: React.FC<ItemCardProps> = ({
|
|||||||
const isCompact = variant === 'compact';
|
const isCompact = variant === 'compact';
|
||||||
|
|
||||||
const getPriceDisplay = () => {
|
const getPriceDisplay = () => {
|
||||||
if (item.pricePerDay !== undefined) {
|
// Collect all available pricing tiers
|
||||||
return Number(item.pricePerDay) === 0
|
const pricingTiers: string[] = [];
|
||||||
? "Free to Borrow"
|
|
||||||
: `$${Math.floor(Number(item.pricePerDay))}/Day`;
|
if (item.pricePerHour && Number(item.pricePerHour) > 0) {
|
||||||
} else if (item.pricePerHour !== undefined) {
|
pricingTiers.push(`$${Math.floor(Number(item.pricePerHour))}/hr`);
|
||||||
return Number(item.pricePerHour) === 0
|
|
||||||
? "Free to Borrow"
|
|
||||||
: `$${Math.floor(Number(item.pricePerHour))}/Hour`;
|
|
||||||
}
|
}
|
||||||
return null;
|
if (item.pricePerDay && Number(item.pricePerDay) > 0) {
|
||||||
|
pricingTiers.push(`$${Math.floor(Number(item.pricePerDay))}/day`);
|
||||||
|
}
|
||||||
|
if (item.pricePerWeek && Number(item.pricePerWeek) > 0) {
|
||||||
|
pricingTiers.push(`$${Math.floor(Number(item.pricePerWeek))}/wk`);
|
||||||
|
}
|
||||||
|
if (item.pricePerMonth && Number(item.pricePerMonth) > 0) {
|
||||||
|
pricingTiers.push(`$${Math.floor(Number(item.pricePerMonth))}/mo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingTiers.length === 0) {
|
||||||
|
return "Free to Borrow";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display up to 2 pricing tiers separated by bullet point
|
||||||
|
return pricingTiers.slice(0, 2).join(" • ");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocationDisplay = () => {
|
const getLocationDisplay = () => {
|
||||||
|
|||||||
@@ -1,51 +1,105 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
interface PricingFormProps {
|
interface PricingFormProps {
|
||||||
priceType: "hour" | "day";
|
|
||||||
pricePerHour: number | string;
|
pricePerHour: number | string;
|
||||||
pricePerDay: number | string;
|
pricePerDay: number | string;
|
||||||
|
pricePerWeek: number | string;
|
||||||
|
pricePerMonth: number | string;
|
||||||
replacementCost: number | string;
|
replacementCost: number | string;
|
||||||
minimumRentalDays: number;
|
selectedPricingUnit: string;
|
||||||
onPriceTypeChange: (type: "hour" | "day") => void;
|
showAdvancedPricing: boolean;
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
|
enabledTiers: {
|
||||||
|
hour: boolean;
|
||||||
|
day: boolean;
|
||||||
|
week: boolean;
|
||||||
|
month: boolean;
|
||||||
|
};
|
||||||
|
onChange: (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => void;
|
||||||
|
onPricingUnitChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||||
|
onToggleAdvancedPricing: () => void;
|
||||||
|
onTierToggle: (tier: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PricingForm: React.FC<PricingFormProps> = ({
|
const PricingForm: React.FC<PricingFormProps> = ({
|
||||||
priceType,
|
|
||||||
pricePerHour,
|
pricePerHour,
|
||||||
pricePerDay,
|
pricePerDay,
|
||||||
|
pricePerWeek,
|
||||||
|
pricePerMonth,
|
||||||
replacementCost,
|
replacementCost,
|
||||||
minimumRentalDays,
|
selectedPricingUnit,
|
||||||
onPriceTypeChange,
|
showAdvancedPricing,
|
||||||
onChange
|
enabledTiers,
|
||||||
|
onChange,
|
||||||
|
onPricingUnitChange,
|
||||||
|
onToggleAdvancedPricing,
|
||||||
|
onTierToggle,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Map pricing unit to field name and label
|
||||||
|
const pricingUnitMap: Record<
|
||||||
|
string,
|
||||||
|
{ field: string; label: string; value: number | string }
|
||||||
|
> = {
|
||||||
|
hour: { field: "pricePerHour", label: "Hour", value: pricePerHour },
|
||||||
|
day: { field: "pricePerDay", label: "Day", value: pricePerDay },
|
||||||
|
week: { field: "pricePerWeek", label: "Week", value: pricePerWeek },
|
||||||
|
month: { field: "pricePerMonth", label: "Month", value: pricePerMonth },
|
||||||
|
};
|
||||||
|
|
||||||
|
// When advanced pricing is on, show ALL 4 options
|
||||||
|
// When advanced pricing is off, show only the 3 non-selected options
|
||||||
|
const advancedPricingOptions = showAdvancedPricing
|
||||||
|
? Object.entries(pricingUnitMap)
|
||||||
|
: Object.entries(pricingUnitMap).filter(
|
||||||
|
([key]) => key !== selectedPricingUnit
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedUnit = pricingUnitMap[selectedPricingUnit];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
<h5 className="card-title">Pricing</h5>
|
||||||
|
<p className="text-muted small mb-3">
|
||||||
|
{showAdvancedPricing
|
||||||
|
? "Set multiple pricing tiers for flexible rental rates."
|
||||||
|
: "Set your primary pricing rate, or use Advanced Pricing for multiple tiers."}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Pricing Unit Dropdown - Only show when advanced pricing is OFF */}
|
||||||
|
{!showAdvancedPricing && (
|
||||||
|
<>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="row align-items-center">
|
<label htmlFor="pricingUnit" className="form-label">
|
||||||
<div className="col-auto">
|
Pricing Unit
|
||||||
<label className="col-form-label">Price per</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div className="col-auto">
|
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={priceType}
|
id="pricingUnit"
|
||||||
onChange={(e) => onPriceTypeChange(e.target.value as "hour" | "day")}
|
value={selectedPricingUnit}
|
||||||
|
onChange={onPricingUnitChange}
|
||||||
>
|
>
|
||||||
<option value="hour">Hour</option>
|
<option value="hour">Hour</option>
|
||||||
<option value="day">Day</option>
|
<option value="day">Day</option>
|
||||||
|
<option value="week">Week</option>
|
||||||
|
<option value="month">Month</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col">
|
|
||||||
|
{/* Primary Price Input */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor={selectedUnit.field} className="form-label mb-0">
|
||||||
|
Price per {selectedUnit.label}
|
||||||
|
</label>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<span className="input-group-text">$</span>
|
<span className="input-group-text">$</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
id={selectedUnit.field}
|
||||||
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
name={selectedUnit.field}
|
||||||
value={priceType === "hour" ? pricePerHour : pricePerDay}
|
value={selectedUnit.value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -53,24 +107,63 @@ const PricingForm: React.FC<PricingFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Advanced Pricing Toggle */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link p-0 text-decoration-none"
|
||||||
|
onClick={onToggleAdvancedPricing}
|
||||||
|
>
|
||||||
|
{showAdvancedPricing ? "▼" : "▶"} Advanced Pricing
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
{/* Advanced Pricing Section */}
|
||||||
<label htmlFor="minimumRentalDays" className="form-label">
|
{showAdvancedPricing && (
|
||||||
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
<div className="border rounded p-3 mb-3 bg-light">
|
||||||
|
<p className="text-muted small mb-3">
|
||||||
|
Set multiple pricing tiers. Check the boxes for the tiers you want to use.
|
||||||
|
</p>
|
||||||
|
{advancedPricingOptions.map(([key, { field, label, value }]) => (
|
||||||
|
<div className="mb-3" key={key}>
|
||||||
|
<div className="form-check mb-2">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id={`enable-${key}`}
|
||||||
|
checked={enabledTiers[key as keyof typeof enabledTiers]}
|
||||||
|
onChange={() => onTierToggle(key)}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={`enable-${key}`}>
|
||||||
|
Price per {label}
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="input-group">
|
||||||
|
<span className="input-group-text">$</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="minimumRentalDays"
|
id={field}
|
||||||
name="minimumRentalDays"
|
name={field}
|
||||||
value={minimumRentalDays}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
min="1"
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
placeholder="0.00"
|
||||||
|
disabled={!enabledTiers[key as keyof typeof enabledTiers]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr className="my-4" />
|
||||||
|
|
||||||
|
{/* Replacement Cost */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label htmlFor="replacementCost" className="form-label mb-0">
|
<label htmlFor="replacementCost" className="form-label mb-0">
|
||||||
Replacement Cost *
|
Replacement Cost *
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ interface ItemFormData {
|
|||||||
inPlaceUseAvailable: boolean;
|
inPlaceUseAvailable: boolean;
|
||||||
pricePerHour?: number | string;
|
pricePerHour?: number | string;
|
||||||
pricePerDay?: number | string;
|
pricePerDay?: number | string;
|
||||||
|
pricePerWeek?: number | string;
|
||||||
|
pricePerMonth?: number | string;
|
||||||
replacementCost: number | string;
|
replacementCost: number | string;
|
||||||
address1: string;
|
address1: string;
|
||||||
address2: string;
|
address2: string;
|
||||||
@@ -28,7 +30,6 @@ interface ItemFormData {
|
|||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rules?: string;
|
rules?: string;
|
||||||
minimumRentalDays: number;
|
|
||||||
needsTraining: boolean;
|
needsTraining: boolean;
|
||||||
generalAvailableAfter: string;
|
generalAvailableAfter: string;
|
||||||
generalAvailableBefore: string;
|
generalAvailableBefore: string;
|
||||||
@@ -62,7 +63,6 @@ const CreateItem: React.FC = () => {
|
|||||||
state: "",
|
state: "",
|
||||||
zipCode: "",
|
zipCode: "",
|
||||||
country: "US",
|
country: "US",
|
||||||
minimumRentalDays: 1,
|
|
||||||
needsTraining: false,
|
needsTraining: false,
|
||||||
generalAvailableAfter: "09:00",
|
generalAvailableAfter: "09:00",
|
||||||
generalAvailableBefore: "17:00",
|
generalAvailableBefore: "17:00",
|
||||||
@@ -79,10 +79,17 @@ const CreateItem: React.FC = () => {
|
|||||||
});
|
});
|
||||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
|
||||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||||
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
||||||
const [addressesLoading, setAddressesLoading] = useState(true);
|
const [addressesLoading, setAddressesLoading] = useState(true);
|
||||||
|
const [selectedPricingUnit, setSelectedPricingUnit] = useState<string>("day");
|
||||||
|
const [showAdvancedPricing, setShowAdvancedPricing] = useState<boolean>(false);
|
||||||
|
const [enabledPricingTiers, setEnabledPricingTiers] = useState({
|
||||||
|
hour: false,
|
||||||
|
day: false,
|
||||||
|
week: false,
|
||||||
|
month: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Reference to LocationForm geocoding function
|
// Reference to LocationForm geocoding function
|
||||||
const geocodeLocationRef = useRef<(() => Promise<boolean>) | null>(null);
|
const geocodeLocationRef = useRef<(() => Promise<boolean>) | null>(null);
|
||||||
@@ -187,6 +194,12 @@ const CreateItem: React.FC = () => {
|
|||||||
pricePerHour: formData.pricePerHour
|
pricePerHour: formData.pricePerHour
|
||||||
? parseFloat(formData.pricePerHour.toString())
|
? parseFloat(formData.pricePerHour.toString())
|
||||||
: undefined,
|
: undefined,
|
||||||
|
pricePerWeek: formData.pricePerWeek
|
||||||
|
? parseFloat(formData.pricePerWeek.toString())
|
||||||
|
: undefined,
|
||||||
|
pricePerMonth: formData.pricePerMonth
|
||||||
|
? parseFloat(formData.pricePerMonth.toString())
|
||||||
|
: undefined,
|
||||||
replacementCost: formData.replacementCost
|
replacementCost: formData.replacementCost
|
||||||
? parseFloat(formData.replacementCost.toString())
|
? parseFloat(formData.replacementCost.toString())
|
||||||
: 0,
|
: 0,
|
||||||
@@ -355,6 +368,21 @@ const CreateItem: React.FC = () => {
|
|||||||
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
|
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePricingUnitChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSelectedPricingUnit(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAdvancedPricing = () => {
|
||||||
|
setShowAdvancedPricing((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTierToggle = (tier: string) => {
|
||||||
|
setEnabledPricingTiers((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tier]: !prev[tier as keyof typeof prev],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
@@ -430,13 +458,18 @@ const CreateItem: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PricingForm
|
<PricingForm
|
||||||
priceType={priceType}
|
|
||||||
pricePerHour={formData.pricePerHour || ""}
|
pricePerHour={formData.pricePerHour || ""}
|
||||||
pricePerDay={formData.pricePerDay || ""}
|
pricePerDay={formData.pricePerDay || ""}
|
||||||
|
pricePerWeek={formData.pricePerWeek || ""}
|
||||||
|
pricePerMonth={formData.pricePerMonth || ""}
|
||||||
replacementCost={formData.replacementCost}
|
replacementCost={formData.replacementCost}
|
||||||
minimumRentalDays={formData.minimumRentalDays}
|
selectedPricingUnit={selectedPricingUnit}
|
||||||
onPriceTypeChange={setPriceType}
|
showAdvancedPricing={showAdvancedPricing}
|
||||||
|
enabledTiers={enabledPricingTiers}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onPricingUnitChange={handlePricingUnitChange}
|
||||||
|
onToggleAdvancedPricing={handleToggleAdvancedPricing}
|
||||||
|
onTierToggle={handleTierToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RulesForm
|
<RulesForm
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ interface ItemFormData {
|
|||||||
inPlaceUseAvailable: boolean;
|
inPlaceUseAvailable: boolean;
|
||||||
pricePerHour?: number | string;
|
pricePerHour?: number | string;
|
||||||
pricePerDay?: number | string;
|
pricePerDay?: number | string;
|
||||||
|
pricePerWeek?: number | string;
|
||||||
|
pricePerMonth?: number | string;
|
||||||
replacementCost: number | string;
|
replacementCost: number | string;
|
||||||
address1: string;
|
address1: string;
|
||||||
address2: string;
|
address2: string;
|
||||||
@@ -28,7 +30,6 @@ interface ItemFormData {
|
|||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rules?: string;
|
rules?: string;
|
||||||
minimumRentalDays: number;
|
|
||||||
needsTraining: boolean;
|
needsTraining: boolean;
|
||||||
generalAvailableAfter: string;
|
generalAvailableAfter: string;
|
||||||
generalAvailableBefore: string;
|
generalAvailableBefore: string;
|
||||||
@@ -53,11 +54,18 @@ const EditItem: React.FC = () => {
|
|||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
|
||||||
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
|
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
|
||||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||||
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
||||||
const [addressesLoading, setAddressesLoading] = useState(true);
|
const [addressesLoading, setAddressesLoading] = useState(true);
|
||||||
|
const [selectedPricingUnit, setSelectedPricingUnit] = useState<string>("day");
|
||||||
|
const [showAdvancedPricing, setShowAdvancedPricing] = useState<boolean>(false);
|
||||||
|
const [enabledPricingTiers, setEnabledPricingTiers] = useState({
|
||||||
|
hour: false,
|
||||||
|
day: false,
|
||||||
|
week: false,
|
||||||
|
month: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Reference to LocationForm geocoding function
|
// Reference to LocationForm geocoding function
|
||||||
const geocodeLocationRef = useRef<(() => Promise<boolean>) | null>(null);
|
const geocodeLocationRef = useRef<(() => Promise<boolean>) | null>(null);
|
||||||
@@ -76,7 +84,6 @@ const EditItem: React.FC = () => {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
country: "US",
|
country: "US",
|
||||||
rules: "",
|
rules: "",
|
||||||
minimumRentalDays: 1,
|
|
||||||
needsTraining: false,
|
needsTraining: false,
|
||||||
generalAvailableAfter: "09:00",
|
generalAvailableAfter: "09:00",
|
||||||
generalAvailableBefore: "17:00",
|
generalAvailableBefore: "17:00",
|
||||||
@@ -119,13 +126,6 @@ const EditItem: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the price type based on available pricing
|
|
||||||
if (item.pricePerHour) {
|
|
||||||
setPriceType("hour");
|
|
||||||
} else if (item.pricePerDay) {
|
|
||||||
setPriceType("day");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert item data to form data format
|
// Convert item data to form data format
|
||||||
setFormData({
|
setFormData({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -134,6 +134,8 @@ const EditItem: React.FC = () => {
|
|||||||
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
|
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
|
||||||
pricePerHour: item.pricePerHour || "",
|
pricePerHour: item.pricePerHour || "",
|
||||||
pricePerDay: item.pricePerDay || "",
|
pricePerDay: item.pricePerDay || "",
|
||||||
|
pricePerWeek: item.pricePerWeek || "",
|
||||||
|
pricePerMonth: item.pricePerMonth || "",
|
||||||
replacementCost: item.replacementCost || "",
|
replacementCost: item.replacementCost || "",
|
||||||
address1: item.address1 || "",
|
address1: item.address1 || "",
|
||||||
address2: item.address2 || "",
|
address2: item.address2 || "",
|
||||||
@@ -144,7 +146,6 @@ const EditItem: React.FC = () => {
|
|||||||
latitude: item.latitude,
|
latitude: item.latitude,
|
||||||
longitude: item.longitude,
|
longitude: item.longitude,
|
||||||
rules: item.rules || "",
|
rules: item.rules || "",
|
||||||
minimumRentalDays: item.minimumRentalDays,
|
|
||||||
needsTraining: item.needsTraining || false,
|
needsTraining: item.needsTraining || false,
|
||||||
generalAvailableAfter: item.availableAfter || "09:00",
|
generalAvailableAfter: item.availableAfter || "09:00",
|
||||||
generalAvailableBefore: item.availableBefore || "17:00",
|
generalAvailableBefore: item.availableBefore || "17:00",
|
||||||
@@ -164,6 +165,40 @@ const EditItem: React.FC = () => {
|
|||||||
if (item.images && item.images.length > 0) {
|
if (item.images && item.images.length > 0) {
|
||||||
setImagePreviews(item.images);
|
setImagePreviews(item.images);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine which pricing unit to select based on existing data
|
||||||
|
// Priority: hour -> day -> week -> month (first one with a value)
|
||||||
|
if (item.pricePerHour) {
|
||||||
|
setSelectedPricingUnit("hour");
|
||||||
|
} else if (item.pricePerDay) {
|
||||||
|
setSelectedPricingUnit("day");
|
||||||
|
} else if (item.pricePerWeek) {
|
||||||
|
setSelectedPricingUnit("week");
|
||||||
|
} else if (item.pricePerMonth) {
|
||||||
|
setSelectedPricingUnit("month");
|
||||||
|
} else {
|
||||||
|
setSelectedPricingUnit("day"); // Default to day if no pricing is set
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set enabled tiers based on which prices are populated
|
||||||
|
setEnabledPricingTiers({
|
||||||
|
hour: !!(item.pricePerHour && Number(item.pricePerHour) > 0),
|
||||||
|
day: !!(item.pricePerDay && Number(item.pricePerDay) > 0),
|
||||||
|
week: !!(item.pricePerWeek && Number(item.pricePerWeek) > 0),
|
||||||
|
month: !!(item.pricePerMonth && Number(item.pricePerMonth) > 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-expand advanced section if multiple pricing tiers are set
|
||||||
|
const pricingTiersSet = [
|
||||||
|
item.pricePerHour,
|
||||||
|
item.pricePerDay,
|
||||||
|
item.pricePerWeek,
|
||||||
|
item.pricePerMonth,
|
||||||
|
].filter((price) => price && Number(price) > 0).length;
|
||||||
|
|
||||||
|
if (pricingTiersSet > 1) {
|
||||||
|
setShowAdvancedPricing(true);
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || "Failed to fetch item");
|
setError(err.response?.data?.message || "Failed to fetch item");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -240,6 +275,12 @@ const EditItem: React.FC = () => {
|
|||||||
pricePerHour: formData.pricePerHour
|
pricePerHour: formData.pricePerHour
|
||||||
? parseFloat(formData.pricePerHour.toString())
|
? parseFloat(formData.pricePerHour.toString())
|
||||||
: undefined,
|
: undefined,
|
||||||
|
pricePerWeek: formData.pricePerWeek
|
||||||
|
? parseFloat(formData.pricePerWeek.toString())
|
||||||
|
: undefined,
|
||||||
|
pricePerMonth: formData.pricePerMonth
|
||||||
|
? parseFloat(formData.pricePerMonth.toString())
|
||||||
|
: undefined,
|
||||||
replacementCost: formData.replacementCost
|
replacementCost: formData.replacementCost
|
||||||
? parseFloat(formData.replacementCost.toString())
|
? parseFloat(formData.replacementCost.toString())
|
||||||
: 0,
|
: 0,
|
||||||
@@ -360,6 +401,21 @@ const EditItem: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePricingUnitChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSelectedPricingUnit(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAdvancedPricing = () => {
|
||||||
|
setShowAdvancedPricing((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTierToggle = (tier: string) => {
|
||||||
|
setEnabledPricingTiers((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tier]: !prev[tier as keyof typeof prev],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mt-5">
|
<div className="container mt-5">
|
||||||
@@ -445,13 +501,18 @@ const EditItem: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PricingForm
|
<PricingForm
|
||||||
priceType={priceType}
|
|
||||||
pricePerHour={formData.pricePerHour || ""}
|
pricePerHour={formData.pricePerHour || ""}
|
||||||
pricePerDay={formData.pricePerDay || ""}
|
pricePerDay={formData.pricePerDay || ""}
|
||||||
|
pricePerWeek={formData.pricePerWeek || ""}
|
||||||
|
pricePerMonth={formData.pricePerMonth || ""}
|
||||||
replacementCost={formData.replacementCost}
|
replacementCost={formData.replacementCost}
|
||||||
minimumRentalDays={formData.minimumRentalDays}
|
selectedPricingUnit={selectedPricingUnit}
|
||||||
onPriceTypeChange={setPriceType}
|
showAdvancedPricing={showAdvancedPricing}
|
||||||
|
enabledTiers={enabledPricingTiers}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onPricingUnitChange={handlePricingUnitChange}
|
||||||
|
onToggleAdvancedPricing={handleToggleAdvancedPricing}
|
||||||
|
onTierToggle={handleTierToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const ItemDetail: React.FC = () => {
|
|||||||
endTime: "12:00",
|
endTime: "12:00",
|
||||||
});
|
});
|
||||||
const [totalCost, setTotalCost] = useState(0);
|
const [totalCost, setTotalCost] = useState(0);
|
||||||
|
const [costLoading, setCostLoading] = useState(false);
|
||||||
|
const [costError, setCostError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItem();
|
fetchItem();
|
||||||
@@ -83,31 +85,37 @@ const ItemDetail: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotalCost = () => {
|
const calculateTotalCost = async () => {
|
||||||
if (!item || !rentalDates.startDate || !rentalDates.endDate) {
|
if (!item || !rentalDates.startDate || !rentalDates.endDate) {
|
||||||
setTotalCost(0);
|
setTotalCost(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCostLoading(true);
|
||||||
|
setCostError(null);
|
||||||
|
|
||||||
const startDateTime = new Date(
|
const startDateTime = new Date(
|
||||||
`${rentalDates.startDate}T${rentalDates.startTime}`
|
`${rentalDates.startDate}T${rentalDates.startTime}`
|
||||||
);
|
).toISOString();
|
||||||
|
|
||||||
const endDateTime = new Date(
|
const endDateTime = new Date(
|
||||||
`${rentalDates.endDate}T${rentalDates.endTime}`
|
`${rentalDates.endDate}T${rentalDates.endTime}`
|
||||||
);
|
).toISOString();
|
||||||
|
|
||||||
const diffMs = endDateTime.getTime() - startDateTime.getTime();
|
const response = await rentalAPI.getRentalCostPreview({
|
||||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
itemId: item.id,
|
||||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
startDateTime,
|
||||||
|
endDateTime,
|
||||||
|
});
|
||||||
|
|
||||||
let cost = 0;
|
setTotalCost(response.data.baseAmount);
|
||||||
if (item.pricePerHour && diffHours <= 24) {
|
} catch (err: any) {
|
||||||
cost = diffHours * Number(item.pricePerHour);
|
setCostError(err.response?.data?.error || "Failed to calculate cost");
|
||||||
} else if (item.pricePerDay) {
|
setTotalCost(0);
|
||||||
cost = diffDays * Number(item.pricePerDay);
|
} finally {
|
||||||
|
setCostLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalCost(cost);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateTimeOptions = (item: Item | null, selectedDate: string) => {
|
const generateTimeOptions = (item: Item | null, selectedDate: string) => {
|
||||||
@@ -458,7 +466,7 @@ const ItemDetail: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{item.pricePerMonth !== undefined &&
|
{item.pricePerMonth !== undefined &&
|
||||||
Number(item.pricePerMonth) > 0 && (
|
Number(item.pricePerMonth) > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-2">
|
||||||
<h4>
|
<h4>
|
||||||
${Math.floor(Number(item.pricePerMonth))}/Month
|
${Math.floor(Number(item.pricePerMonth))}/Month
|
||||||
</h4>
|
</h4>
|
||||||
@@ -571,11 +579,17 @@ const ItemDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{rentalDates.startDate &&
|
{rentalDates.startDate && rentalDates.endDate && (
|
||||||
rentalDates.endDate &&
|
|
||||||
totalCost > 0 && (
|
|
||||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||||
|
{costLoading ? (
|
||||||
|
<div className="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span className="visually-hidden">Calculating...</span>
|
||||||
|
</div>
|
||||||
|
) : costError ? (
|
||||||
|
<small className="text-danger">{costError}</small>
|
||||||
|
) : totalCost > 0 ? (
|
||||||
<strong>Total: ${totalCost}</strong>
|
<strong>Total: ${totalCost}</strong>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ const Owning: React.FC = () => {
|
|||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<h4 className="mb-3">
|
<h4 className="mb-3">
|
||||||
<i className="bi bi-calendar-check me-2"></i>
|
<i className="bi bi-calendar-check me-2"></i>
|
||||||
Rental Requests
|
Rentals
|
||||||
</h4>
|
</h4>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{allOwnerRentals.map((rental) => (
|
{allOwnerRentals.map((rental) => (
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ const RentItem: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [totalCost, setTotalCost] = useState(0);
|
const [totalCost, setTotalCost] = useState(0);
|
||||||
|
const [costLoading, setCostLoading] = useState(false);
|
||||||
|
const [costError, setCostError] = useState<string | null>(null);
|
||||||
const [completed, setCompleted] = useState(false);
|
const [completed, setCompleted] = useState(false);
|
||||||
|
|
||||||
const convertToUTC = (dateString: string, timeString: string): string => {
|
const convertToUTC = (dateString: string, timeString: string): string => {
|
||||||
@@ -62,31 +64,37 @@ const RentItem: React.FC = () => {
|
|||||||
return `${hour12}:${minute} ${period}`;
|
return `${hour12}:${minute} ${period}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotalCost = () => {
|
const calculateTotalCost = async () => {
|
||||||
if (!item || !manualSelection.startDate || !manualSelection.endDate) {
|
if (!item || !manualSelection.startDate || !manualSelection.endDate) {
|
||||||
setTotalCost(0);
|
setTotalCost(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCostLoading(true);
|
||||||
|
setCostError(null);
|
||||||
|
|
||||||
const startDateTime = new Date(
|
const startDateTime = new Date(
|
||||||
`${manualSelection.startDate}T${manualSelection.startTime}`
|
`${manualSelection.startDate}T${manualSelection.startTime}`
|
||||||
);
|
).toISOString();
|
||||||
|
|
||||||
const endDateTime = new Date(
|
const endDateTime = new Date(
|
||||||
`${manualSelection.endDate}T${manualSelection.endTime}`
|
`${manualSelection.endDate}T${manualSelection.endTime}`
|
||||||
);
|
).toISOString();
|
||||||
|
|
||||||
const diffMs = endDateTime.getTime() - startDateTime.getTime();
|
const response = await rentalAPI.getRentalCostPreview({
|
||||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
itemId: item.id,
|
||||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
startDateTime,
|
||||||
|
endDateTime,
|
||||||
|
});
|
||||||
|
|
||||||
let cost = 0;
|
setTotalCost(response.data.baseAmount);
|
||||||
if (item.pricePerHour && diffHours <= 24) {
|
} catch (err: any) {
|
||||||
cost = diffHours * Number(item.pricePerHour);
|
setCostError(err.response?.data?.error || "Failed to calculate cost");
|
||||||
} else if (item.pricePerDay) {
|
setTotalCost(0);
|
||||||
cost = diffDays * Number(item.pricePerDay);
|
} finally {
|
||||||
|
setCostLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalCost(cost);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -366,15 +374,23 @@ const RentItem: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Total Cost */}
|
{/* Total Cost */}
|
||||||
{totalCost > 0 && (
|
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="d-flex justify-content-between">
|
<div className="d-flex justify-content-between">
|
||||||
<strong>Total:</strong>
|
<strong>Total:</strong>
|
||||||
|
{costLoading ? (
|
||||||
|
<div className="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span className="visually-hidden">Calculating...</span>
|
||||||
|
</div>
|
||||||
|
) : costError ? (
|
||||||
|
<small className="text-danger">Error</small>
|
||||||
|
) : totalCost > 0 ? (
|
||||||
<strong>${totalCost}</strong>
|
<strong>${totalCost}</strong>
|
||||||
|
) : (
|
||||||
|
<strong>$0</strong>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -229,6 +229,11 @@ export const rentalAPI = {
|
|||||||
) => api.post(`/rentals/${id}/mark-return`, data),
|
) => api.post(`/rentals/${id}/mark-return`, data),
|
||||||
reportDamage: (id: string, data: any) =>
|
reportDamage: (id: string, data: any) =>
|
||||||
api.post(`/rentals/${id}/report-damage`, data),
|
api.post(`/rentals/${id}/report-damage`, data),
|
||||||
|
getRentalCostPreview: (data: {
|
||||||
|
itemId: string;
|
||||||
|
startDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
}) => api.post("/rentals/cost-preview", data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageAPI = {
|
export const messageAPI = {
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ export interface ItemRequest {
|
|||||||
longitude?: number;
|
longitude?: number;
|
||||||
maxPricePerHour?: number;
|
maxPricePerHour?: number;
|
||||||
maxPricePerDay?: number;
|
maxPricePerDay?: number;
|
||||||
|
maxPricePerWeek?: number;
|
||||||
|
maxPricePerMonth?: number;
|
||||||
preferredStartDate?: string;
|
preferredStartDate?: string;
|
||||||
preferredEndDate?: string;
|
preferredEndDate?: string;
|
||||||
isFlexibleDates: boolean;
|
isFlexibleDates: boolean;
|
||||||
@@ -273,6 +275,8 @@ export interface ItemRequestResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
offerPricePerHour?: number;
|
offerPricePerHour?: number;
|
||||||
offerPricePerDay?: number;
|
offerPricePerDay?: number;
|
||||||
|
offerPricePerWeek?: number;
|
||||||
|
offerPricePerMonth?: number;
|
||||||
availableStartDate?: string;
|
availableStartDate?: string;
|
||||||
availableEndDate?: string;
|
availableEndDate?: string;
|
||||||
existingItemId?: string;
|
existingItemId?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user