pricing tiers
This commit is contained in:
@@ -18,6 +18,8 @@ interface ItemFormData {
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number | string;
|
||||
pricePerDay?: number | string;
|
||||
pricePerWeek?: number | string;
|
||||
pricePerMonth?: number | string;
|
||||
replacementCost: number | string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
@@ -28,7 +30,6 @@ interface ItemFormData {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
rules?: string;
|
||||
minimumRentalDays: number;
|
||||
needsTraining: boolean;
|
||||
generalAvailableAfter: string;
|
||||
generalAvailableBefore: string;
|
||||
@@ -62,7 +63,6 @@ const CreateItem: React.FC = () => {
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "US",
|
||||
minimumRentalDays: 1,
|
||||
needsTraining: false,
|
||||
generalAvailableAfter: "09:00",
|
||||
generalAvailableBefore: "17:00",
|
||||
@@ -79,11 +79,18 @@ const CreateItem: React.FC = () => {
|
||||
});
|
||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
||||
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
|
||||
const geocodeLocationRef = useRef<(() => Promise<boolean>) | null>(null);
|
||||
|
||||
@@ -187,6 +194,12 @@ const CreateItem: React.FC = () => {
|
||||
pricePerHour: formData.pricePerHour
|
||||
? parseFloat(formData.pricePerHour.toString())
|
||||
: undefined,
|
||||
pricePerWeek: formData.pricePerWeek
|
||||
? parseFloat(formData.pricePerWeek.toString())
|
||||
: undefined,
|
||||
pricePerMonth: formData.pricePerMonth
|
||||
? parseFloat(formData.pricePerMonth.toString())
|
||||
: undefined,
|
||||
replacementCost: formData.replacementCost
|
||||
? parseFloat(formData.replacementCost.toString())
|
||||
: 0,
|
||||
@@ -355,6 +368,21 @@ const CreateItem: React.FC = () => {
|
||||
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 (
|
||||
<div className="container mt-4">
|
||||
<div className="row justify-content-center">
|
||||
@@ -430,13 +458,18 @@ const CreateItem: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<PricingForm
|
||||
priceType={priceType}
|
||||
pricePerHour={formData.pricePerHour || ""}
|
||||
pricePerDay={formData.pricePerDay || ""}
|
||||
pricePerWeek={formData.pricePerWeek || ""}
|
||||
pricePerMonth={formData.pricePerMonth || ""}
|
||||
replacementCost={formData.replacementCost}
|
||||
minimumRentalDays={formData.minimumRentalDays}
|
||||
onPriceTypeChange={setPriceType}
|
||||
selectedPricingUnit={selectedPricingUnit}
|
||||
showAdvancedPricing={showAdvancedPricing}
|
||||
enabledTiers={enabledPricingTiers}
|
||||
onChange={handleChange}
|
||||
onPricingUnitChange={handlePricingUnitChange}
|
||||
onToggleAdvancedPricing={handleToggleAdvancedPricing}
|
||||
onTierToggle={handleTierToggle}
|
||||
/>
|
||||
|
||||
<RulesForm
|
||||
|
||||
@@ -18,6 +18,8 @@ interface ItemFormData {
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number | string;
|
||||
pricePerDay?: number | string;
|
||||
pricePerWeek?: number | string;
|
||||
pricePerMonth?: number | string;
|
||||
replacementCost: number | string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
@@ -28,7 +30,6 @@ interface ItemFormData {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
rules?: string;
|
||||
minimumRentalDays: number;
|
||||
needsTraining: boolean;
|
||||
generalAvailableAfter: string;
|
||||
generalAvailableBefore: string;
|
||||
@@ -53,12 +54,19 @@ const EditItem: React.FC = () => {
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
||||
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
|
||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
|
||||
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
|
||||
const geocodeLocationRef = useRef<(() => Promise<boolean>) | null>(null);
|
||||
const [formData, setFormData] = useState<ItemFormData>({
|
||||
@@ -76,7 +84,6 @@ const EditItem: React.FC = () => {
|
||||
zipCode: "",
|
||||
country: "US",
|
||||
rules: "",
|
||||
minimumRentalDays: 1,
|
||||
needsTraining: false,
|
||||
generalAvailableAfter: "09:00",
|
||||
generalAvailableBefore: "17:00",
|
||||
@@ -119,13 +126,6 @@ const EditItem: React.FC = () => {
|
||||
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
|
||||
setFormData({
|
||||
name: item.name,
|
||||
@@ -134,6 +134,8 @@ const EditItem: React.FC = () => {
|
||||
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
|
||||
pricePerHour: item.pricePerHour || "",
|
||||
pricePerDay: item.pricePerDay || "",
|
||||
pricePerWeek: item.pricePerWeek || "",
|
||||
pricePerMonth: item.pricePerMonth || "",
|
||||
replacementCost: item.replacementCost || "",
|
||||
address1: item.address1 || "",
|
||||
address2: item.address2 || "",
|
||||
@@ -144,7 +146,6 @@ const EditItem: React.FC = () => {
|
||||
latitude: item.latitude,
|
||||
longitude: item.longitude,
|
||||
rules: item.rules || "",
|
||||
minimumRentalDays: item.minimumRentalDays,
|
||||
needsTraining: item.needsTraining || false,
|
||||
generalAvailableAfter: item.availableAfter || "09:00",
|
||||
generalAvailableBefore: item.availableBefore || "17:00",
|
||||
@@ -164,6 +165,40 @@ const EditItem: React.FC = () => {
|
||||
if (item.images && item.images.length > 0) {
|
||||
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) {
|
||||
setError(err.response?.data?.message || "Failed to fetch item");
|
||||
} finally {
|
||||
@@ -240,6 +275,12 @@ const EditItem: React.FC = () => {
|
||||
pricePerHour: formData.pricePerHour
|
||||
? parseFloat(formData.pricePerHour.toString())
|
||||
: undefined,
|
||||
pricePerWeek: formData.pricePerWeek
|
||||
? parseFloat(formData.pricePerWeek.toString())
|
||||
: undefined,
|
||||
pricePerMonth: formData.pricePerMonth
|
||||
? parseFloat(formData.pricePerMonth.toString())
|
||||
: undefined,
|
||||
replacementCost: formData.replacementCost
|
||||
? parseFloat(formData.replacementCost.toString())
|
||||
: 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) {
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
@@ -445,13 +501,18 @@ const EditItem: React.FC = () => {
|
||||
/>
|
||||
|
||||
<PricingForm
|
||||
priceType={priceType}
|
||||
pricePerHour={formData.pricePerHour || ""}
|
||||
pricePerDay={formData.pricePerDay || ""}
|
||||
pricePerWeek={formData.pricePerWeek || ""}
|
||||
pricePerMonth={formData.pricePerMonth || ""}
|
||||
replacementCost={formData.replacementCost}
|
||||
minimumRentalDays={formData.minimumRentalDays}
|
||||
onPriceTypeChange={setPriceType}
|
||||
selectedPricingUnit={selectedPricingUnit}
|
||||
showAdvancedPricing={showAdvancedPricing}
|
||||
enabledTiers={enabledPricingTiers}
|
||||
onChange={handleChange}
|
||||
onPricingUnitChange={handlePricingUnitChange}
|
||||
onToggleAdvancedPricing={handleToggleAdvancedPricing}
|
||||
onTierToggle={handleTierToggle}
|
||||
/>
|
||||
|
||||
<div className="card mb-4">
|
||||
|
||||
@@ -22,6 +22,8 @@ const ItemDetail: React.FC = () => {
|
||||
endTime: "12:00",
|
||||
});
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
const [costLoading, setCostLoading] = useState(false);
|
||||
const [costError, setCostError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItem();
|
||||
@@ -83,31 +85,37 @@ const ItemDetail: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const calculateTotalCost = () => {
|
||||
const calculateTotalCost = async () => {
|
||||
if (!item || !rentalDates.startDate || !rentalDates.endDate) {
|
||||
setTotalCost(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const startDateTime = new Date(
|
||||
`${rentalDates.startDate}T${rentalDates.startTime}`
|
||||
);
|
||||
const endDateTime = new Date(
|
||||
`${rentalDates.endDate}T${rentalDates.endTime}`
|
||||
);
|
||||
try {
|
||||
setCostLoading(true);
|
||||
setCostError(null);
|
||||
|
||||
const diffMs = endDateTime.getTime() - startDateTime.getTime();
|
||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
const startDateTime = new Date(
|
||||
`${rentalDates.startDate}T${rentalDates.startTime}`
|
||||
).toISOString();
|
||||
|
||||
let cost = 0;
|
||||
if (item.pricePerHour && diffHours <= 24) {
|
||||
cost = diffHours * Number(item.pricePerHour);
|
||||
} else if (item.pricePerDay) {
|
||||
cost = diffDays * Number(item.pricePerDay);
|
||||
const endDateTime = new Date(
|
||||
`${rentalDates.endDate}T${rentalDates.endTime}`
|
||||
).toISOString();
|
||||
|
||||
const response = await rentalAPI.getRentalCostPreview({
|
||||
itemId: item.id,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
|
||||
setTotalCost(response.data.baseAmount);
|
||||
} catch (err: any) {
|
||||
setCostError(err.response?.data?.error || "Failed to calculate cost");
|
||||
setTotalCost(0);
|
||||
} finally {
|
||||
setCostLoading(false);
|
||||
}
|
||||
|
||||
setTotalCost(cost);
|
||||
};
|
||||
|
||||
const generateTimeOptions = (item: Item | null, selectedDate: string) => {
|
||||
@@ -458,7 +466,7 @@ const ItemDetail: React.FC = () => {
|
||||
)}
|
||||
{item.pricePerMonth !== undefined &&
|
||||
Number(item.pricePerMonth) > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<h4>
|
||||
${Math.floor(Number(item.pricePerMonth))}/Month
|
||||
</h4>
|
||||
@@ -571,13 +579,19 @@ const ItemDetail: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rentalDates.startDate &&
|
||||
rentalDates.endDate &&
|
||||
totalCost > 0 && (
|
||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||
{rentalDates.startDate && rentalDates.endDate && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -305,7 +305,7 @@ const Owning: React.FC = () => {
|
||||
<div className="mb-5">
|
||||
<h4 className="mb-3">
|
||||
<i className="bi bi-calendar-check me-2"></i>
|
||||
Rental Requests
|
||||
Rentals
|
||||
</h4>
|
||||
<div className="row">
|
||||
{allOwnerRentals.map((rental) => (
|
||||
|
||||
@@ -27,6 +27,8 @@ const RentItem: React.FC = () => {
|
||||
});
|
||||
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
const [costLoading, setCostLoading] = useState(false);
|
||||
const [costError, setCostError] = useState<string | null>(null);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
|
||||
const convertToUTC = (dateString: string, timeString: string): string => {
|
||||
@@ -62,31 +64,37 @@ const RentItem: React.FC = () => {
|
||||
return `${hour12}:${minute} ${period}`;
|
||||
};
|
||||
|
||||
const calculateTotalCost = () => {
|
||||
const calculateTotalCost = async () => {
|
||||
if (!item || !manualSelection.startDate || !manualSelection.endDate) {
|
||||
setTotalCost(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const startDateTime = new Date(
|
||||
`${manualSelection.startDate}T${manualSelection.startTime}`
|
||||
);
|
||||
const endDateTime = new Date(
|
||||
`${manualSelection.endDate}T${manualSelection.endTime}`
|
||||
);
|
||||
try {
|
||||
setCostLoading(true);
|
||||
setCostError(null);
|
||||
|
||||
const diffMs = endDateTime.getTime() - startDateTime.getTime();
|
||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
const startDateTime = new Date(
|
||||
`${manualSelection.startDate}T${manualSelection.startTime}`
|
||||
).toISOString();
|
||||
|
||||
let cost = 0;
|
||||
if (item.pricePerHour && diffHours <= 24) {
|
||||
cost = diffHours * Number(item.pricePerHour);
|
||||
} else if (item.pricePerDay) {
|
||||
cost = diffDays * Number(item.pricePerDay);
|
||||
const endDateTime = new Date(
|
||||
`${manualSelection.endDate}T${manualSelection.endTime}`
|
||||
).toISOString();
|
||||
|
||||
const response = await rentalAPI.getRentalCostPreview({
|
||||
itemId: item.id,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
|
||||
setTotalCost(response.data.baseAmount);
|
||||
} catch (err: any) {
|
||||
setCostError(err.response?.data?.error || "Failed to calculate cost");
|
||||
setTotalCost(0);
|
||||
} finally {
|
||||
setCostLoading(false);
|
||||
}
|
||||
|
||||
setTotalCost(cost);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -366,15 +374,23 @@ const RentItem: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Total Cost */}
|
||||
{totalCost > 0 && (
|
||||
<>
|
||||
<hr />
|
||||
<div className="d-flex justify-content-between">
|
||||
<strong>Total:</strong>
|
||||
<>
|
||||
<hr />
|
||||
<div className="d-flex justify-content-between">
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<strong>$0</strong>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user