condition check gallery
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { conditionCheckAPI } from "../services/api";
|
||||
import { uploadFiles } from "../services/uploadService";
|
||||
import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||
|
||||
interface ConditionCheckModalProps {
|
||||
@@ -83,18 +84,17 @@ const ConditionCheckModal: React.FC<ConditionCheckModalProps> = ({
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("checkType", checkType);
|
||||
if (notes.trim()) {
|
||||
formData.append("notes", notes.trim());
|
||||
}
|
||||
// Upload photos to S3 first
|
||||
const uploadResults = await uploadFiles("condition-check", photos);
|
||||
const imageFilenames = uploadResults.map((result) => result.key);
|
||||
|
||||
photos.forEach((photo, index) => {
|
||||
formData.append("photos", photo);
|
||||
// Submit condition check with S3 keys
|
||||
await conditionCheckAPI.submitConditionCheck(rentalId, {
|
||||
checkType,
|
||||
imageFilenames,
|
||||
notes: notes.trim() || undefined,
|
||||
});
|
||||
|
||||
await conditionCheckAPI.submitConditionCheck(rentalId, formData);
|
||||
|
||||
// Reset form
|
||||
setPhotos([]);
|
||||
setNotes("");
|
||||
@@ -180,8 +180,8 @@ const ConditionCheckModal: React.FC<ConditionCheckModalProps> = ({
|
||||
alt={`Photo ${index + 1}`}
|
||||
className="img-fluid rounded"
|
||||
style={{
|
||||
height: "100px",
|
||||
objectFit: "cover",
|
||||
maxHeight: "120px",
|
||||
objectFit: "contain",
|
||||
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 { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||
import { uploadFiles } from "../services/uploadService";
|
||||
import { Rental } from "../types";
|
||||
|
||||
interface ReturnStatusModalProps {
|
||||
@@ -289,19 +290,15 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
||||
|
||||
// Submit post-rental condition check if photos are provided
|
||||
if (photos.length > 0) {
|
||||
const conditionCheckFormData = new FormData();
|
||||
conditionCheckFormData.append("checkType", "post_rental_owner");
|
||||
if (conditionNotes.trim()) {
|
||||
conditionCheckFormData.append("notes", conditionNotes.trim());
|
||||
}
|
||||
photos.forEach((photo) => {
|
||||
conditionCheckFormData.append("photos", photo);
|
||||
});
|
||||
// Upload photos to S3 first
|
||||
const uploadResults = await uploadFiles("condition-check", photos);
|
||||
const imageFilenames = uploadResults.map((result) => result.key);
|
||||
|
||||
await conditionCheckAPI.submitConditionCheck(
|
||||
rental.id,
|
||||
conditionCheckFormData
|
||||
);
|
||||
await conditionCheckAPI.submitConditionCheck(rental.id, {
|
||||
checkType: "post_rental_owner",
|
||||
imageFilenames,
|
||||
notes: conditionNotes.trim() || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// Determine primary status for API call
|
||||
@@ -643,8 +640,8 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
||||
alt={`Photo ${index + 1}`}
|
||||
className="img-fluid rounded"
|
||||
style={{
|
||||
height: "100px",
|
||||
objectFit: "cover",
|
||||
maxHeight: "120px",
|
||||
objectFit: "contain",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -51,6 +51,7 @@ const EditItem: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||
@@ -258,6 +259,7 @@ const EditItem: React.FC = () => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
|
||||
// Try to geocode the address before submitting
|
||||
let geocodedCoordinates = null;
|
||||
@@ -341,6 +343,8 @@ const EditItem: React.FC = () => {
|
||||
}, 1500);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || err.message || "Failed to update item");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -573,9 +577,9 @@ const EditItem: React.FC = () => {
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
disabled={submitting}
|
||||
>
|
||||
{loading ? "Updating..." : "Update Listing"}
|
||||
{submitting ? "Updating..." : "Update Listing"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -2,13 +2,14 @@ import React, { useState, useEffect } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import api from "../services/api";
|
||||
import { Item, Rental } from "../types";
|
||||
import { Item, Rental, ConditionCheck } from "../types";
|
||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||
import { getPublicImageUrl } from "../services/uploadService";
|
||||
import ReviewRenterModal from "../components/ReviewRenterModal";
|
||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||
import DeclineRentalModal from "../components/DeclineRentalModal";
|
||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||
import ReturnStatusModal from "../components/ReturnStatusModal";
|
||||
|
||||
const Owning: React.FC = () => {
|
||||
@@ -63,6 +64,11 @@ const Owning: React.FC = () => {
|
||||
checkType: string;
|
||||
} | null>(null);
|
||||
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 [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
||||
|
||||
@@ -72,6 +78,12 @@ const Owning: React.FC = () => {
|
||||
fetchAvailableChecks();
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ownerRentals.length > 0) {
|
||||
fetchConditionChecks();
|
||||
}
|
||||
}, [ownerRentals]);
|
||||
|
||||
const fetchListings = async () => {
|
||||
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
|
||||
const handleAcceptRental = async (rentalId: string) => {
|
||||
try {
|
||||
@@ -252,7 +286,12 @@ const Owning: React.FC = () => {
|
||||
|
||||
const handleConditionCheckSuccess = () => {
|
||||
fetchAvailableChecks();
|
||||
alert("Condition check submitted successfully!");
|
||||
fetchConditionChecks();
|
||||
};
|
||||
|
||||
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||
setSelectedConditionCheck(check);
|
||||
setShowConditionCheckViewer(true);
|
||||
};
|
||||
|
||||
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)
|
||||
const allOwnerRentals = ownerRentals
|
||||
.filter((r) => ["pending", "confirmed", "active"].includes(r.status))
|
||||
@@ -307,14 +351,19 @@ const Owning: React.FC = () => {
|
||||
{allOwnerRentals.map((rental) => (
|
||||
<div key={rental.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<div className="card h-100">
|
||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
|
||||
/>
|
||||
)}
|
||||
{rental.item?.imageFilenames &&
|
||||
rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{
|
||||
height: "200px",
|
||||
objectFit: "contain",
|
||||
backgroundColor: "#f8f9fa",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-dark">
|
||||
{rental.item ? rental.item.name : "Item Unavailable"}
|
||||
@@ -472,6 +521,34 @@ const Owning: React.FC = () => {
|
||||
)}
|
||||
</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 */}
|
||||
{getAvailableChecksForRental(rental.id).map((check) => (
|
||||
<button
|
||||
@@ -483,8 +560,8 @@ const Owning: React.FC = () => {
|
||||
>
|
||||
<i className="bi bi-camera me-2" />
|
||||
{check.checkType === "pre_rental_owner"
|
||||
? "Submit Pre-Rental Check"
|
||||
: "Submit Post-Rental Check"}
|
||||
? "Submit Pre-Rental Condition"
|
||||
: "Submit Post-Rental Condition"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -533,7 +610,11 @@ const Owning: React.FC = () => {
|
||||
src={getPublicImageUrl(item.imageFilenames[0])}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
|
||||
style={{
|
||||
height: "200px",
|
||||
objectFit: "contain",
|
||||
backgroundColor: "#f8f9fa",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
@@ -712,6 +793,16 @@ const Owning: React.FC = () => {
|
||||
onSubmitSuccess={handleReturnStatusMarked}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Condition Check Viewer Modal */}
|
||||
<ConditionCheckViewerModal
|
||||
show={showConditionCheckViewer}
|
||||
onHide={() => {
|
||||
setShowConditionCheckViewer(false);
|
||||
setSelectedConditionCheck(null);
|
||||
}}
|
||||
conditionCheck={selectedConditionCheck}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,11 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||
import { getPublicImageUrl } from "../services/uploadService";
|
||||
import { Rental } from "../types";
|
||||
import { Rental, ConditionCheck } from "../types";
|
||||
import ReviewItemModal from "../components/ReviewModal";
|
||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||
|
||||
const Renting: React.FC = () => {
|
||||
// Helper function to format time
|
||||
@@ -53,7 +54,11 @@ const Renting: React.FC = () => {
|
||||
checkType: string;
|
||||
} | null>(null);
|
||||
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(() => {
|
||||
fetchRentals();
|
||||
@@ -149,7 +154,11 @@ const Renting: React.FC = () => {
|
||||
const handleConditionCheckSuccess = () => {
|
||||
fetchAvailableChecks();
|
||||
fetchConditionChecks();
|
||||
alert("Condition check submitted successfully!");
|
||||
};
|
||||
|
||||
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||
setSelectedConditionCheck(check);
|
||||
setShowConditionCheckViewer(true);
|
||||
};
|
||||
|
||||
const getAvailableChecksForRental = (rentalId: string) => {
|
||||
@@ -231,14 +240,19 @@ const Renting: React.FC = () => {
|
||||
className="card h-100"
|
||||
style={{ cursor: rental.item ? "pointer" : "default" }}
|
||||
>
|
||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
|
||||
/>
|
||||
)}
|
||||
{rental.item?.imageFilenames &&
|
||||
rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={getPublicImageUrl(rental.item.imageFilenames[0])}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{
|
||||
height: "200px",
|
||||
objectFit: "contain",
|
||||
backgroundColor: "#f8f9fa",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-dark">
|
||||
{rental.item ? rental.item.name : "Item Unavailable"}
|
||||
@@ -367,20 +381,20 @@ const Renting: React.FC = () => {
|
||||
<div className="mb-2">
|
||||
{getCompletedChecksForRental(rental.id).map(
|
||||
(check) => (
|
||||
<div
|
||||
<button
|
||||
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"
|
||||
? "Start Check Completed"
|
||||
: "End Check Completed"}
|
||||
? "Rental Start Condition Noted"
|
||||
: "Rental End Condition Noted"}
|
||||
<small className="text-muted ms-2">
|
||||
{new Date(
|
||||
check.createdAt
|
||||
).toLocaleDateString()}
|
||||
</small>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -397,8 +411,8 @@ const Renting: React.FC = () => {
|
||||
>
|
||||
<i className="bi bi-camera me-2" />
|
||||
{check.checkType === "rental_start_renter"
|
||||
? "Submit Start Check"
|
||||
: "Submit End Check"}
|
||||
? "Submit Rental Start Condition"
|
||||
: "Submit Rental End Condition"}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -472,6 +486,16 @@ const Renting: React.FC = () => {
|
||||
onSuccess={handleConditionCheckSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Condition Check Viewer Modal */}
|
||||
<ConditionCheckViewerModal
|
||||
show={showConditionCheckViewer}
|
||||
onHide={() => {
|
||||
setShowConditionCheckViewer(false);
|
||||
setSelectedConditionCheck(null);
|
||||
}}
|
||||
conditionCheck={selectedConditionCheck}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -332,10 +332,10 @@ export const mapsAPI = {
|
||||
};
|
||||
|
||||
export const conditionCheckAPI = {
|
||||
submitConditionCheck: (rentalId: string, formData: FormData) =>
|
||||
api.post(`/condition-checks/${rentalId}`, formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
}),
|
||||
submitConditionCheck: (
|
||||
rentalId: string,
|
||||
data: { checkType: string; imageFilenames: string[]; notes?: string }
|
||||
) => api.post(`/condition-checks/${rentalId}`, data),
|
||||
getConditionChecks: (rentalId: string) =>
|
||||
api.get(`/condition-checks/${rentalId}`),
|
||||
getConditionCheckTimeline: (rentalId: string) =>
|
||||
|
||||
Reference in New Issue
Block a user