condition check gallery

This commit is contained in:
jackiettran
2025-12-16 13:50:23 -05:00
parent 372ab093ef
commit 27a7b641dd
8 changed files with 421 additions and 64 deletions

View File

@@ -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%",
}}
/>

View 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;

View File

@@ -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%",
}}
/>