condition check gallery
This commit is contained in:
@@ -177,7 +177,16 @@ router.get(
|
|||||||
requireS3Enabled,
|
requireS3Enabled,
|
||||||
async (req, res, next) => {
|
async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { key } = req.params;
|
// Express wildcard params may be string or array - handle both
|
||||||
|
let key = req.params.key;
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
key = key.join("/");
|
||||||
|
}
|
||||||
|
if (!key || typeof key !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid key parameter" });
|
||||||
|
}
|
||||||
|
// Decode URL-encoded characters (e.g., %2F -> /)
|
||||||
|
key = decodeURIComponent(key);
|
||||||
|
|
||||||
// Only allow private folders to use signed URLs
|
// Only allow private folders to use signed URLs
|
||||||
const isPrivate =
|
const isPrivate =
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { conditionCheckAPI } from "../services/api";
|
import { conditionCheckAPI } from "../services/api";
|
||||||
|
import { uploadFiles } from "../services/uploadService";
|
||||||
import { IMAGE_LIMITS } from "../config/imageLimits";
|
import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||||
|
|
||||||
interface ConditionCheckModalProps {
|
interface ConditionCheckModalProps {
|
||||||
@@ -83,18 +84,17 @@ const ConditionCheckModal: React.FC<ConditionCheckModalProps> = ({
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const formData = new FormData();
|
// Upload photos to S3 first
|
||||||
formData.append("checkType", checkType);
|
const uploadResults = await uploadFiles("condition-check", photos);
|
||||||
if (notes.trim()) {
|
const imageFilenames = uploadResults.map((result) => result.key);
|
||||||
formData.append("notes", notes.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
photos.forEach((photo, index) => {
|
// Submit condition check with S3 keys
|
||||||
formData.append("photos", photo);
|
await conditionCheckAPI.submitConditionCheck(rentalId, {
|
||||||
|
checkType,
|
||||||
|
imageFilenames,
|
||||||
|
notes: notes.trim() || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await conditionCheckAPI.submitConditionCheck(rentalId, formData);
|
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
setPhotos([]);
|
setPhotos([]);
|
||||||
setNotes("");
|
setNotes("");
|
||||||
@@ -180,8 +180,8 @@ const ConditionCheckModal: React.FC<ConditionCheckModalProps> = ({
|
|||||||
alt={`Photo ${index + 1}`}
|
alt={`Photo ${index + 1}`}
|
||||||
className="img-fluid rounded"
|
className="img-fluid rounded"
|
||||||
style={{
|
style={{
|
||||||
height: "100px",
|
maxHeight: "120px",
|
||||||
objectFit: "cover",
|
objectFit: "contain",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
232
frontend/src/components/ConditionCheckViewerModal.tsx
Normal file
232
frontend/src/components/ConditionCheckViewerModal.tsx
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { ConditionCheck } from "../types";
|
||||||
|
import { getSignedUrl } from "../services/uploadService";
|
||||||
|
|
||||||
|
interface ConditionCheckViewerModalProps {
|
||||||
|
show: boolean;
|
||||||
|
onHide: () => void;
|
||||||
|
conditionCheck: ConditionCheck | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkTypeLabels: Record<string, string> = {
|
||||||
|
pre_rental_owner: "Pre-Rental Check (Owner)",
|
||||||
|
rental_start_renter: "Rental Start Check (Renter)",
|
||||||
|
rental_end_renter: "Rental End Check (Renter)",
|
||||||
|
post_rental_owner: "Post-Rental Check (Owner)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConditionCheckViewerModal: React.FC<ConditionCheckViewerModalProps> = ({
|
||||||
|
show,
|
||||||
|
onHide,
|
||||||
|
conditionCheck,
|
||||||
|
}) => {
|
||||||
|
const [imageUrls, setImageUrls] = useState<Map<string, string>>(new Map());
|
||||||
|
const [selectedImage, setSelectedImage] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchImageUrls = async () => {
|
||||||
|
if (!conditionCheck?.imageFilenames?.length) return;
|
||||||
|
|
||||||
|
// Filter to only valid string keys
|
||||||
|
const validKeys = conditionCheck.imageFilenames.filter(
|
||||||
|
(key): key is string => typeof key === "string" && key.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validKeys.length === 0) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const newUrls = new Map<string, string>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
validKeys.map(async (key) => {
|
||||||
|
const url = await getSignedUrl(key);
|
||||||
|
newUrls.set(key, url);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setImageUrls(newUrls);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch image URLs:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (show && conditionCheck) {
|
||||||
|
setSelectedImage(0);
|
||||||
|
fetchImageUrls();
|
||||||
|
}
|
||||||
|
}, [show, conditionCheck]);
|
||||||
|
|
||||||
|
if (!show || !conditionCheck) return null;
|
||||||
|
|
||||||
|
const submitterName = conditionCheck.submittedByUser
|
||||||
|
? `${conditionCheck.submittedByUser.firstName} ${conditionCheck.submittedByUser.lastName}`
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
|
const submittedDate = new Date(conditionCheck.submittedAt).toLocaleString(undefined, {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter to only valid string keys for display
|
||||||
|
const validImageKeys = (conditionCheck.imageFilenames || []).filter(
|
||||||
|
(key): key is string => typeof key === "string" && key.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentImageKey = validImageKeys[selectedImage];
|
||||||
|
const currentImageUrl = currentImageKey ? imageUrls.get(currentImageKey) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal d-block"
|
||||||
|
tabIndex={-1}
|
||||||
|
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onHide();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h5 className="modal-title">
|
||||||
|
<i className="bi bi-camera me-2" />
|
||||||
|
{checkTypeLabels[conditionCheck.checkType] || "Condition Check"}
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={onHide}
|
||||||
|
aria-label="Close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-body">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-5">
|
||||||
|
<div className="spinner-border" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-muted">Loading images...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Main Image */}
|
||||||
|
<div
|
||||||
|
className="text-center mb-3"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#f8f9fa",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentImageUrl ? (
|
||||||
|
<img
|
||||||
|
src={currentImageUrl}
|
||||||
|
alt={`Condition check photo ${selectedImage + 1}`}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "400px",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted py-5">
|
||||||
|
<i className="bi bi-image fs-1" />
|
||||||
|
<p>Image not available</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thumbnail Strip */}
|
||||||
|
{validImageKeys.length > 1 && (
|
||||||
|
<div className="d-flex gap-2 overflow-auto justify-content-center mb-3">
|
||||||
|
{validImageKeys.map((key, index) => {
|
||||||
|
const thumbUrl = imageUrls.get(key);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => setSelectedImage(index)}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
border:
|
||||||
|
selectedImage === index
|
||||||
|
? "2px solid #0d6efd"
|
||||||
|
: "2px solid transparent",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "2px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{thumbUrl ? (
|
||||||
|
<img
|
||||||
|
src={thumbUrl}
|
||||||
|
alt={`Thumbnail ${index + 1}`}
|
||||||
|
style={{
|
||||||
|
width: "60px",
|
||||||
|
height: "60px",
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="bg-secondary d-flex align-items-center justify-content-center"
|
||||||
|
style={{
|
||||||
|
width: "60px",
|
||||||
|
height: "60px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bi bi-image text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image Counter */}
|
||||||
|
<p className="text-center text-muted small mb-3">
|
||||||
|
Photo {selectedImage + 1} of {validImageKeys.length}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
{conditionCheck.notes && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<h6>
|
||||||
|
<i className="bi bi-chat-left-text me-2" />
|
||||||
|
Notes
|
||||||
|
</h6>
|
||||||
|
<p className="mb-0 text-muted">{conditionCheck.notes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="border-top pt-3">
|
||||||
|
<small className="text-muted">
|
||||||
|
<i className="bi bi-person me-1" />
|
||||||
|
Submitted by <strong>{submitterName}</strong>
|
||||||
|
<span className="mx-2">|</span>
|
||||||
|
<i className="bi bi-calendar me-1" />
|
||||||
|
{submittedDate}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onHide}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConditionCheckViewerModal;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||||
|
import { uploadFiles } from "../services/uploadService";
|
||||||
import { Rental } from "../types";
|
import { Rental } from "../types";
|
||||||
|
|
||||||
interface ReturnStatusModalProps {
|
interface ReturnStatusModalProps {
|
||||||
@@ -289,19 +290,15 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
|||||||
|
|
||||||
// Submit post-rental condition check if photos are provided
|
// Submit post-rental condition check if photos are provided
|
||||||
if (photos.length > 0) {
|
if (photos.length > 0) {
|
||||||
const conditionCheckFormData = new FormData();
|
// Upload photos to S3 first
|
||||||
conditionCheckFormData.append("checkType", "post_rental_owner");
|
const uploadResults = await uploadFiles("condition-check", photos);
|
||||||
if (conditionNotes.trim()) {
|
const imageFilenames = uploadResults.map((result) => result.key);
|
||||||
conditionCheckFormData.append("notes", conditionNotes.trim());
|
|
||||||
}
|
|
||||||
photos.forEach((photo) => {
|
|
||||||
conditionCheckFormData.append("photos", photo);
|
|
||||||
});
|
|
||||||
|
|
||||||
await conditionCheckAPI.submitConditionCheck(
|
await conditionCheckAPI.submitConditionCheck(rental.id, {
|
||||||
rental.id,
|
checkType: "post_rental_owner",
|
||||||
conditionCheckFormData
|
imageFilenames,
|
||||||
);
|
notes: conditionNotes.trim() || undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine primary status for API call
|
// Determine primary status for API call
|
||||||
@@ -643,8 +640,8 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
|||||||
alt={`Photo ${index + 1}`}
|
alt={`Photo ${index + 1}`}
|
||||||
className="img-fluid rounded"
|
className="img-fluid rounded"
|
||||||
style={{
|
style={{
|
||||||
height: "100px",
|
maxHeight: "120px",
|
||||||
objectFit: "cover",
|
objectFit: "contain",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const EditItem: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
@@ -258,6 +259,7 @@ const EditItem: React.FC = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
// Try to geocode the address before submitting
|
// Try to geocode the address before submitting
|
||||||
let geocodedCoordinates = null;
|
let geocodedCoordinates = null;
|
||||||
@@ -341,6 +343,8 @@ const EditItem: React.FC = () => {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || err.message || "Failed to update item");
|
setError(err.response?.data?.message || err.message || "Failed to update item");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -573,9 +577,9 @@ const EditItem: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
disabled={loading}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{loading ? "Updating..." : "Update Listing"}
|
{submitting ? "Updating..." : "Update Listing"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import api from "../services/api";
|
import api from "../services/api";
|
||||||
import { Item, Rental } from "../types";
|
import { Item, Rental, ConditionCheck } from "../types";
|
||||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||||
import { getPublicImageUrl } from "../services/uploadService";
|
import { getPublicImageUrl } from "../services/uploadService";
|
||||||
import ReviewRenterModal from "../components/ReviewRenterModal";
|
import ReviewRenterModal from "../components/ReviewRenterModal";
|
||||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||||
import DeclineRentalModal from "../components/DeclineRentalModal";
|
import DeclineRentalModal from "../components/DeclineRentalModal";
|
||||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||||
|
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||||
import ReturnStatusModal from "../components/ReturnStatusModal";
|
import ReturnStatusModal from "../components/ReturnStatusModal";
|
||||||
|
|
||||||
const Owning: React.FC = () => {
|
const Owning: React.FC = () => {
|
||||||
@@ -63,6 +64,11 @@ const Owning: React.FC = () => {
|
|||||||
checkType: string;
|
checkType: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
|
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
|
||||||
|
const [conditionChecks, setConditionChecks] = useState<ConditionCheck[]>([]);
|
||||||
|
const [showConditionCheckViewer, setShowConditionCheckViewer] =
|
||||||
|
useState(false);
|
||||||
|
const [selectedConditionCheck, setSelectedConditionCheck] =
|
||||||
|
useState<ConditionCheck | null>(null);
|
||||||
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
|
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
|
||||||
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
||||||
|
|
||||||
@@ -72,6 +78,12 @@ const Owning: React.FC = () => {
|
|||||||
fetchAvailableChecks();
|
fetchAvailableChecks();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ownerRentals.length > 0) {
|
||||||
|
fetchConditionChecks();
|
||||||
|
}
|
||||||
|
}, [ownerRentals]);
|
||||||
|
|
||||||
const fetchListings = async () => {
|
const fetchListings = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
@@ -145,6 +157,28 @@ const Owning: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchConditionChecks = async () => {
|
||||||
|
try {
|
||||||
|
const allChecks: ConditionCheck[] = [];
|
||||||
|
for (const rental of ownerRentals) {
|
||||||
|
try {
|
||||||
|
const response = await conditionCheckAPI.getConditionChecks(
|
||||||
|
rental.id
|
||||||
|
);
|
||||||
|
if (response.data.conditionChecks) {
|
||||||
|
allChecks.push(...response.data.conditionChecks);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Skip rentals with no condition checks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConditionChecks(allChecks);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch condition checks:", err);
|
||||||
|
setConditionChecks([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Owner functionality handlers
|
// Owner functionality handlers
|
||||||
const handleAcceptRental = async (rentalId: string) => {
|
const handleAcceptRental = async (rentalId: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -252,7 +286,12 @@ const Owning: React.FC = () => {
|
|||||||
|
|
||||||
const handleConditionCheckSuccess = () => {
|
const handleConditionCheckSuccess = () => {
|
||||||
fetchAvailableChecks();
|
fetchAvailableChecks();
|
||||||
alert("Condition check submitted successfully!");
|
fetchConditionChecks();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||||
|
setSelectedConditionCheck(check);
|
||||||
|
setShowConditionCheckViewer(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailableChecksForRental = (rentalId: string) => {
|
const getAvailableChecksForRental = (rentalId: string) => {
|
||||||
@@ -263,6 +302,11 @@ const Owning: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCompletedChecksForRental = (rentalId: string) => {
|
||||||
|
if (!Array.isArray(conditionChecks)) return [];
|
||||||
|
return conditionChecks.filter((check) => check.rentalId === rentalId);
|
||||||
|
};
|
||||||
|
|
||||||
// Filter owner rentals - exclude cancelled (shown in Rental History)
|
// Filter owner rentals - exclude cancelled (shown in Rental History)
|
||||||
const allOwnerRentals = ownerRentals
|
const allOwnerRentals = ownerRentals
|
||||||
.filter((r) => ["pending", "confirmed", "active"].includes(r.status))
|
.filter((r) => ["pending", "confirmed", "active"].includes(r.status))
|
||||||
@@ -307,14 +351,19 @@ const Owning: React.FC = () => {
|
|||||||
{allOwnerRentals.map((rental) => (
|
{allOwnerRentals.map((rental) => (
|
||||||
<div key={rental.id} className="col-md-6 col-lg-4 mb-4">
|
<div key={rental.id} className="col-md-6 col-lg-4 mb-4">
|
||||||
<div className="card h-100">
|
<div className="card h-100">
|
||||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
{rental.item?.imageFilenames &&
|
||||||
<img
|
rental.item.imageFilenames[0] && (
|
||||||
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
<img
|
||||||
className="card-img-top"
|
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
||||||
alt={rental.item.name}
|
className="card-img-top"
|
||||||
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
|
alt={rental.item.name}
|
||||||
/>
|
style={{
|
||||||
)}
|
height: "200px",
|
||||||
|
objectFit: "contain",
|
||||||
|
backgroundColor: "#f8f9fa",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h5 className="card-title text-dark">
|
<h5 className="card-title text-dark">
|
||||||
{rental.item ? rental.item.name : "Item Unavailable"}
|
{rental.item ? rental.item.name : "Item Unavailable"}
|
||||||
@@ -472,6 +521,34 @@ const Owning: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Condition Check Status */}
|
||||||
|
{getCompletedChecksForRental(rental.id).length > 0 && (
|
||||||
|
<div className="mb-2">
|
||||||
|
{getCompletedChecksForRental(rental.id).map(
|
||||||
|
(check) => (
|
||||||
|
<button
|
||||||
|
key={`${rental.id}-${check.checkType}-status`}
|
||||||
|
className="btn btn-link text-success small p-0 text-decoration-none d-block"
|
||||||
|
onClick={() => handleViewConditionCheck(check)}
|
||||||
|
>
|
||||||
|
{check.checkType === "pre_rental_owner"
|
||||||
|
? "Pre-Rental Condition"
|
||||||
|
: check.checkType === "rental_start_renter"
|
||||||
|
? "Rental Start Condition"
|
||||||
|
: check.checkType === "rental_end_renter"
|
||||||
|
? "Rental End Condition"
|
||||||
|
: "Post-Rental Condition"}
|
||||||
|
<small className="text-muted ms-2">
|
||||||
|
{new Date(
|
||||||
|
check.createdAt
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</small>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Condition Check Buttons */}
|
{/* Condition Check Buttons */}
|
||||||
{getAvailableChecksForRental(rental.id).map((check) => (
|
{getAvailableChecksForRental(rental.id).map((check) => (
|
||||||
<button
|
<button
|
||||||
@@ -483,8 +560,8 @@ const Owning: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<i className="bi bi-camera me-2" />
|
<i className="bi bi-camera me-2" />
|
||||||
{check.checkType === "pre_rental_owner"
|
{check.checkType === "pre_rental_owner"
|
||||||
? "Submit Pre-Rental Check"
|
? "Submit Pre-Rental Condition"
|
||||||
: "Submit Post-Rental Check"}
|
: "Submit Post-Rental Condition"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -533,7 +610,11 @@ const Owning: React.FC = () => {
|
|||||||
src={getPublicImageUrl(item.imageFilenames[0])}
|
src={getPublicImageUrl(item.imageFilenames[0])}
|
||||||
className="card-img-top"
|
className="card-img-top"
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
|
style={{
|
||||||
|
height: "200px",
|
||||||
|
objectFit: "contain",
|
||||||
|
backgroundColor: "#f8f9fa",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
@@ -712,6 +793,16 @@ const Owning: React.FC = () => {
|
|||||||
onSubmitSuccess={handleReturnStatusMarked}
|
onSubmitSuccess={handleReturnStatusMarked}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Condition Check Viewer Modal */}
|
||||||
|
<ConditionCheckViewerModal
|
||||||
|
show={showConditionCheckViewer}
|
||||||
|
onHide={() => {
|
||||||
|
setShowConditionCheckViewer(false);
|
||||||
|
setSelectedConditionCheck(null);
|
||||||
|
}}
|
||||||
|
conditionCheck={selectedConditionCheck}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { Link, useNavigate } from "react-router-dom";
|
|||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||||
import { getPublicImageUrl } from "../services/uploadService";
|
import { getPublicImageUrl } from "../services/uploadService";
|
||||||
import { Rental } from "../types";
|
import { Rental, ConditionCheck } from "../types";
|
||||||
import ReviewItemModal from "../components/ReviewModal";
|
import ReviewItemModal from "../components/ReviewModal";
|
||||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||||
|
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||||
|
|
||||||
const Renting: React.FC = () => {
|
const Renting: React.FC = () => {
|
||||||
// Helper function to format time
|
// Helper function to format time
|
||||||
@@ -53,7 +54,11 @@ const Renting: React.FC = () => {
|
|||||||
checkType: string;
|
checkType: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
|
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
|
||||||
const [conditionChecks, setConditionChecks] = useState<any[]>([]);
|
const [conditionChecks, setConditionChecks] = useState<ConditionCheck[]>([]);
|
||||||
|
const [showConditionCheckViewer, setShowConditionCheckViewer] =
|
||||||
|
useState(false);
|
||||||
|
const [selectedConditionCheck, setSelectedConditionCheck] =
|
||||||
|
useState<ConditionCheck | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRentals();
|
fetchRentals();
|
||||||
@@ -149,7 +154,11 @@ const Renting: React.FC = () => {
|
|||||||
const handleConditionCheckSuccess = () => {
|
const handleConditionCheckSuccess = () => {
|
||||||
fetchAvailableChecks();
|
fetchAvailableChecks();
|
||||||
fetchConditionChecks();
|
fetchConditionChecks();
|
||||||
alert("Condition check submitted successfully!");
|
};
|
||||||
|
|
||||||
|
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||||
|
setSelectedConditionCheck(check);
|
||||||
|
setShowConditionCheckViewer(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailableChecksForRental = (rentalId: string) => {
|
const getAvailableChecksForRental = (rentalId: string) => {
|
||||||
@@ -231,14 +240,19 @@ const Renting: React.FC = () => {
|
|||||||
className="card h-100"
|
className="card h-100"
|
||||||
style={{ cursor: rental.item ? "pointer" : "default" }}
|
style={{ cursor: rental.item ? "pointer" : "default" }}
|
||||||
>
|
>
|
||||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
{rental.item?.imageFilenames &&
|
||||||
<img
|
rental.item.imageFilenames[0] && (
|
||||||
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
<img
|
||||||
className="card-img-top"
|
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
||||||
alt={rental.item.name}
|
className="card-img-top"
|
||||||
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
|
alt={rental.item.name}
|
||||||
/>
|
style={{
|
||||||
)}
|
height: "200px",
|
||||||
|
objectFit: "contain",
|
||||||
|
backgroundColor: "#f8f9fa",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h5 className="card-title text-dark">
|
<h5 className="card-title text-dark">
|
||||||
{rental.item ? rental.item.name : "Item Unavailable"}
|
{rental.item ? rental.item.name : "Item Unavailable"}
|
||||||
@@ -367,20 +381,20 @@ const Renting: React.FC = () => {
|
|||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
{getCompletedChecksForRental(rental.id).map(
|
{getCompletedChecksForRental(rental.id).map(
|
||||||
(check) => (
|
(check) => (
|
||||||
<div
|
<button
|
||||||
key={`${rental.id}-${check.checkType}-status`}
|
key={`${rental.id}-${check.checkType}-status`}
|
||||||
className="text-success small"
|
className="btn btn-link text-success small p-0 text-decoration-none d-block"
|
||||||
|
onClick={() => handleViewConditionCheck(check)}
|
||||||
>
|
>
|
||||||
<i className="bi bi-camera-fill me-1"></i>
|
|
||||||
{check.checkType === "rental_start_renter"
|
{check.checkType === "rental_start_renter"
|
||||||
? "Start Check Completed"
|
? "Rental Start Condition Noted"
|
||||||
: "End Check Completed"}
|
: "Rental End Condition Noted"}
|
||||||
<small className="text-muted ms-2">
|
<small className="text-muted ms-2">
|
||||||
{new Date(
|
{new Date(
|
||||||
check.createdAt
|
check.createdAt
|
||||||
).toLocaleDateString()}
|
).toLocaleDateString()}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -397,8 +411,8 @@ const Renting: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<i className="bi bi-camera me-2" />
|
<i className="bi bi-camera me-2" />
|
||||||
{check.checkType === "rental_start_renter"
|
{check.checkType === "rental_start_renter"
|
||||||
? "Submit Start Check"
|
? "Submit Rental Start Condition"
|
||||||
: "Submit End Check"}
|
: "Submit Rental End Condition"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -472,6 +486,16 @@ const Renting: React.FC = () => {
|
|||||||
onSuccess={handleConditionCheckSuccess}
|
onSuccess={handleConditionCheckSuccess}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Condition Check Viewer Modal */}
|
||||||
|
<ConditionCheckViewerModal
|
||||||
|
show={showConditionCheckViewer}
|
||||||
|
onHide={() => {
|
||||||
|
setShowConditionCheckViewer(false);
|
||||||
|
setSelectedConditionCheck(null);
|
||||||
|
}}
|
||||||
|
conditionCheck={selectedConditionCheck}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -332,10 +332,10 @@ export const mapsAPI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const conditionCheckAPI = {
|
export const conditionCheckAPI = {
|
||||||
submitConditionCheck: (rentalId: string, formData: FormData) =>
|
submitConditionCheck: (
|
||||||
api.post(`/condition-checks/${rentalId}`, formData, {
|
rentalId: string,
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
data: { checkType: string; imageFilenames: string[]; notes?: string }
|
||||||
}),
|
) => api.post(`/condition-checks/${rentalId}`, data),
|
||||||
getConditionChecks: (rentalId: string) =>
|
getConditionChecks: (rentalId: string) =>
|
||||||
api.get(`/condition-checks/${rentalId}`),
|
api.get(`/condition-checks/${rentalId}`),
|
||||||
getConditionCheckTimeline: (rentalId: string) =>
|
getConditionCheckTimeline: (rentalId: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user