diff --git a/src/actions/bmdashboard/equipmentActions.js b/src/actions/bmdashboard/equipmentActions.js index 7e604125ad..35ebee7c31 100644 --- a/src/actions/bmdashboard/equipmentActions.js +++ b/src/actions/bmdashboard/equipmentActions.js @@ -26,7 +26,7 @@ export const setErrors = payload => { }; export const fetchEquipmentById = equipmentId => { - const url = ENDPOINTS.BM_EQUIPMENT_BY_ID(equipmentId); + const url = `${ENDPOINTS.BM_EQUIPMENT_BY_ID(equipmentId)}`; return async dispatch => { axios .get(url) @@ -77,12 +77,12 @@ export const purchaseEquipment = async body => { export const updateMultipleEquipmentLogs = (projectId, bulkArr) => dispatch => { axios .put( - `${ENDPOINTS.BM_EQUIPMENT_LOGS}?project=${projectId}`, + `${ENDPOINTS.BM_EQUIPMENT_LOGS}?project=${projectId}`, bulkArr ) .then(res => { - dispatch(setEquipments(res.data)); - toast.success('Equipment logs updated successfully!'); + dispatch(setEquipments(res.data)); + toast.success('Equipment logs updated successfully!'); return res.data; }) .catch(err => { @@ -91,3 +91,71 @@ export const updateMultipleEquipmentLogs = (projectId, bulkArr) => dispatch => { throw err; }); } + +export const updateEquipment = (equipmentId, updateData) => async (dispatch, getState) => { + const url = `${ENDPOINTS.BM_EQUIPMENT_STATUS_UPDATE(equipmentId)}`; + + try { + const state = getState(); + let currentUserId = state?.auth?.user?.userid; + + if (!currentUserId) { + currentUserId = state?.auth?.user?._id || + state?.auth?.user?.id || + state?.auth?._id || + state?.auth?.id; + } + + if (!currentUserId) { + const storedUserId = localStorage.getItem('userId'); + if (storedUserId) { + currentUserId = storedUserId; + } + } + + if (!currentUserId) { + console.error('No user ID found in any storage location'); + console.error('Auth state:', state.auth); + + const errorMsg = 'User not authenticated. Please log in.'; + toast.error(errorMsg); + throw new Error(errorMsg); + } + + const statusUpdateData = { + condition: updateData.condition, + lastUsedBy: updateData.lastUsedBy, + lastUsedFor: updateData.lastUsedFor, + replacementRequired: updateData.replacementRequired, + description: updateData.description || '', + notes: updateData.notes || '', + createdBy: currentUserId, + }; + + const res = await axios.put(url, statusUpdateData); + + dispatch(setEquipment(res.data)); + toast.success('Equipment status updated successfully!'); + dispatch(fetchEquipmentById(equipmentId)); + + return res.data; + } catch (err) { + + let errorMessage = 'Failed to update equipment status.'; + + if (err.response) { + errorMessage = err.response.data?.error || + err.response.data?.message || + err.response.statusText || + errorMessage; + dispatch(setErrors(err.response.data)); + } else if (err.request) { + errorMessage = 'No response from server. Please check your connection.'; + } else { + errorMessage = err.message; + } + + toast.error(errorMessage); + throw err; + } +}; \ No newline at end of file diff --git a/src/components/BMDashboard/Equipment/Update/UpdateEquipment.jsx b/src/components/BMDashboard/Equipment/Update/UpdateEquipment.jsx index 122b48fb89..86107e8171 100644 --- a/src/components/BMDashboard/Equipment/Update/UpdateEquipment.jsx +++ b/src/components/BMDashboard/Equipment/Update/UpdateEquipment.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchEquipmentById } from '~/actions/bmdashboard/equipmentActions'; -import { Button, Form, FormGroup, Label, Container, Row, Col, Input } from 'reactstrap'; +import { fetchEquipmentById, updateEquipment } from '~/actions/bmdashboard/equipmentActions'; +import { Button, Form, FormGroup, Label, Container, Row, Col, Input, Alert } from 'reactstrap'; import CheckTypesModal from '~/components/BMDashboard/shared/CheckTypesModal'; import { useHistory, useParams } from 'react-router-dom'; import Radio from '~/components/common/Radio'; @@ -24,14 +24,37 @@ export default function UpdateEquipment() { const [status, setStatus] = useState(''); const [notes, setNotes] = useState(''); const [uploadedFiles, setUploadedFiles] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(''); + const [submitSuccess, setSubmitSuccess] = useState(''); + const [uploadedFilesPreview, setUploadedFilesPreview] = useState([]); const equipmentDetails = useSelector(state => state.bmEquipments.singleEquipment); - const dispatch = useDispatch(); + useEffect(() => { if (equipmentId) { dispatch(fetchEquipmentById(equipmentId)); } + + return () => { + uploadedFilesPreview.forEach(file => { + if (file.preview && file.preview.startsWith('blob:')) { + URL.revokeObjectURL(file.preview); + } + }); + }; }, [dispatch, equipmentId]); + + useEffect(() => { + return () => { + uploadedFilesPreview.forEach(file => { + if (file.preview && file.preview.startsWith('blob:')) { + URL.revokeObjectURL(file.preview); + } + }); + }; + }, [uploadedFilesPreview]); + const handleCancel = () => history.goBack(); const calculateDaysLeft = endDate => { @@ -45,63 +68,269 @@ export default function UpdateEquipment() { return daysLeft >= 0 ? daysLeft : 'Expired'; }; - const handleSubmit = e => { + const validateForm = () => { + if (!status) { + return 'Status/Condition is required'; + } + if (!lastUsedBy) { + return 'Please specify who used the tool/equipment last time'; + } + if (lastUsedBy === 'other' && !lastUsedByOther.trim()) { + return 'Please specify the name of who used the tool/equipment'; + } + if (!lastUsedFor) { + return 'Please specify what the tool/equipment was used for'; + } + if (lastUsedFor === 'other' && !lastUsedForOther.trim()) { + return 'Please specify what the tool/equipment was used for'; + } + if (!replacementRequired) { + return 'Please indicate if replacement is required'; + } + if (sendNote === 'yes' && !notes.trim()) { + return 'Please add a note if you selected to send one'; + } + return null; + }; + + const handleFileUpload = files => { + uploadedFilesPreview.forEach(file => { + if (file.preview && file.preview.startsWith('blob:')) { + URL.revokeObjectURL(file.preview); + } + }); + + const validFiles = files.filter(file => { + const fileType = file.type || ''; + const fileName = file.name || ''; + const isValidType = fileType.startsWith('image/'); + const isValidExtension = fileName.match(/\.(jpg|jpeg|png|gif|webp)$/i); + return isValidType || isValidExtension; + }); + + if (validFiles.length === 0) { + console.warn('No valid image files found'); + return; + } + + const newPreviews = validFiles.map(file => { + const previewUrl = URL.createObjectURL(file); + return { + name: file.name || 'Image', + preview: previewUrl, + size: file.size || 0, + type: file.type || 'image/*', + file: file, + status: 'uploaded', + uploadedAt: new Date().toISOString(), + }; + }); + + setUploadedFiles(prev => [...prev, ...validFiles]); + setUploadedFilesPreview(prev => [...prev, ...newPreviews]); + }; + + const formatFileSize = bytes => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const handleSubmit = async e => { e.preventDefault(); + + const validationError = validateForm(); + if (validationError) { + setSubmitError(validationError); + return; + } + + setIsSubmitting(true); + setSubmitError(''); + setSubmitSuccess(''); + + try { + const updateData = { + condition: status, + lastUsedBy: lastUsedBy === 'other' ? lastUsedByOther : lastUsedBy, + lastUsedFor: lastUsedFor === 'other' ? lastUsedForOther : lastUsedFor, + replacementRequired, + description, + notes: sendNote === 'yes' ? notes : '', + }; + + await dispatch(updateEquipment(equipmentId, updateData)); + + let successMessage = 'Equipment status updated successfully!'; + + if (uploadedFiles.length > 0) { + successMessage += ' Note: Images were uploaded and stored locally for preview.'; + setUploadedFilesPreview(prev => + prev.map(file => ({ + ...file, + status: 'local-only', + message: 'Stored locally for preview', + })), + ); + } + + setSubmitSuccess(successMessage); + + setTimeout(() => { + history.goBack(); + }, 2000); + } catch (error) { + console.error('Update failed:', error); + + let errorMessage = 'Failed to update equipment. Please try again.'; + + if (error.message && error.message.includes('User not authenticated')) { + errorMessage = 'Authentication error. Please check if you are logged in.'; + } else if (error.response?.data?.error?.includes('Invalid user ID format')) { + errorMessage = 'User ID format error. Please contact support.'; + } + + if (uploadedFiles.length > 0) { + errorMessage += ' Note: Images were selected and previewed but not saved to database.'; + setUploadedFilesPreview(prev => + prev.map(file => ({ + ...file, + status: 'not-saved', + message: 'Selected but not saved to database', + })), + ); + } + + setSubmitError(errorMessage); + } finally { + setIsSubmitting(false); + } + }; + + const handleRemoveFile = index => { + if (uploadedFilesPreview[index]?.preview) { + URL.revokeObjectURL(uploadedFilesPreview[index].preview); + } + + const newPreviews = [...uploadedFilesPreview]; + newPreviews.splice(index, 1); + setUploadedFilesPreview(newPreviews); + + const newFiles = [...uploadedFiles]; + newFiles.splice(index, 1); + setUploadedFiles(newFiles); + }; + + const formLabelStyle = { + fontWeight: '600', + color: '#212529', + display: 'block', + marginBottom: '0.5rem', + }; + + const formInputStyle = { + borderColor: '#ced4da', + backgroundColor: '#ffffff', + color: '#212529', + }; + + const readonlyStyle = { + padding: '10px 12px', + backgroundColor: '#ffffff', + border: '1px solid #ced4da', + borderRadius: '6px', + color: '#212529', + fontSize: '1rem', + minHeight: '48px', + display: 'flex', + alignItems: 'center', + marginBottom: '5px', + fontWeight: '500', }; return ( - +
-

Update Tool or Equipment Status

+

Update Tool or Equipment Status

- {equipmentDetails && ( + {submitError && ( + + + + {submitError} + + + + )} + + {submitSuccess && ( + + + + + {submitSuccess} + + + + )} + + {equipmentDetails && ( + Equipment image )} -
+ + - -
- {equipmentDetails?.itemType?.name || 'Unknown'} -
+ +
{equipmentDetails?.itemType?.name || 'Unknown'}
- -
{equipmentDetails?._id || 'Unknown'}
+ +
{equipmentDetails?._id || 'Unknown'}
- -
- {equipmentDetails?.itemType?.category || 'Unknown'} -
+ +
{equipmentDetails?.itemType?.category || 'Unknown'}
- -
- {equipmentDetails?.project?.name || 'Unknown'} -
+ +
{equipmentDetails?.project?.name || 'Unknown'}
- -
+ +
{equipmentDetails?.updateRecord?.length > 0 ? equipmentDetails.updateRecord[equipmentDetails.updateRecord.length - 1] .condition @@ -109,132 +338,158 @@ export default function UpdateEquipment() {
- -
- {equipmentDetails?.purchaseStatus || 'Unknown'} -
+ +
{equipmentDetails?.purchaseStatus || 'Unknown'}
{equipmentDetails && equipmentDetails.purchaseStatus === 'Rental' && ( - -
+ +
{equipmentDetails.rentalDueDate?.split('T')[0] || 'Unknown'}
- -
+ +
{calculateDaysLeft(equipmentDetails?.rentalDueDate)}
)} + -
+
Please confirm you are updating the status of the tool or equipment shown above.
- - - - - setUpdateDate(e.target.value)} - placeholder="DD/MM/YYYY" - className="form-control" - /> - - - + - + setStatus(e.target.value)} + required + style={formInputStyle} > - - - - - + + + + + + + - + setLastUsedBy(e.target.value)} + required + style={formInputStyle} > - - - - + + + + + - setLastUsedByOther(e.target.value)} - className="mt-2" - /> + {lastUsedBy === 'other' && ( + setLastUsedByOther(e.target.value)} + className="mt-2" + required + style={formInputStyle} + /> + )} + - + setLastUsedFor(e.target.value)} + required + style={formInputStyle} > - - - - + + + + + - setLastUsedForOther(e.target.value)} - className="mt-2" - /> + {lastUsedFor === 'other' && ( + setLastUsedForOther(e.target.value)} + className="mt-2" + required + style={formInputStyle} + /> + )} - - - + setDescription(e.target.value)} + style={formInputStyle} /> - - - - setNotes(e.target.value)} - /> - + {sendNote === 'yes' && ( + + + setNotes(e.target.value)} + required={sendNote === 'yes'} + style={formInputStyle} + /> + + )}
@@ -301,11 +702,39 @@ export default function UpdateEquipment() { color="secondary" className="bm-dashboard__button btn btn-secondary" onClick={handleCancel} + disabled={isSubmitting} + style={{ backgroundColor: '#6c757d', borderColor: '#6c757d' }} > Cancel -
diff --git a/src/components/BMDashboard/Equipment/Update/UpdateEquipment.module.css b/src/components/BMDashboard/Equipment/Update/UpdateEquipment.module.css index 980f3e3cc4..c7bf743797 100644 --- a/src/components/BMDashboard/Equipment/Update/UpdateEquipment.module.css +++ b/src/components/BMDashboard/Equipment/Update/UpdateEquipment.module.css @@ -1,46 +1,69 @@ -.dragDropContainer { - border: 2px dashed #ccc; - text-align: center; - padding: 20px; - margin-bottom: 20px; -} - -.updateConfirmText { - color: red; - font-weight: bold; - margin-bottom: 10px; -} - -.cancelButton { - width: 100%; -} +/* UpdateEquipment.module.css - Minimal and Working */ -.submitButton { - width: 100%; +.readOnlyDiv { + padding: 10px 12px; + background-color: #ffffff !important; + border-radius: 6px; + border: 1px solid #ced4da !important; + color: #212529 !important; + font-size: 1rem; + min-height: 48px; + display: flex; + align-items: center; + margin-bottom: 5px; + font-weight: 500; } +/* Image */ .squareImage { - width: 100%; - height: auto; - max-width: 250px; - max-height: 250px; + width: 280px; + height: 280px; object-fit: cover; - border-radius: 4px; + border-radius: 8px; + border: 2px solid #dee2e6 !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -.squareImage { - width: 100%; - height: auto; - max-width: 250px; - max-height: 250px; - object-fit: cover; - border-radius: 4px; +/* Confirmation text */ +.updateConfirmText { + color: #0d6efd !important; + font-weight: 600; + margin: 20px 0; + padding: 12px 16px; + background-color: #f8f9fa !important; + border: 1px solid #cfe2ff !important; + border-radius: 6px; + font-size: 1rem; } -.readOnlyDiv { - padding: 8px; - background-color: #e6f5f2; - border-radius: 4px; - border: 1px solid #dee2e6; - color: #495057; +/* Additional styling for form labels in light mode */ +:global(.form-label) { + color: #212529 !important; + font-weight: 600 !important; } + +/* ========================= + DARK MODE - Only change what's needed + ========================= */ +@media (prefers-color-scheme: dark) { + .readOnlyDiv { + background-color: #2d2d2d !important; + border-color: #444444 !important; + color: #e9ecef !important; + } + + .squareImage { + border-color: #444444 !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + + .updateConfirmText { + color: #0dcaf0 !important; + background-color: #1a2a3a !important; + border-color: #0dcaf0 !important; + } + + :global(.form-label) { + color: #e9ecef !important; + } +} \ No newline at end of file diff --git a/src/components/common/DragAndDrop/DragAndDrop.css b/src/components/common/DragAndDrop/DragAndDrop.css deleted file mode 100644 index 447edca7d0..0000000000 --- a/src/components/common/DragAndDrop/DragAndDrop.css +++ /dev/null @@ -1,60 +0,0 @@ -#file-upload-form{ - height: 16rem; - width: 100%; - text-align: center; - position: relative; -} - -#file-upload-input{ - display: none; -} - -#file-upload-label{ - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border-width: 2px; - border-radius: 1rem; - border-style: dashed; - border-color: #cbd5e1; - background-color: #ffffff; -} - -#file-upload-label.drag-active{ - background-color: #f8fafc; -} - -#drag-file-element { - position: absolute; - width: 100%; - height: 100%; - border-radius: 1rem; - top: 0px; - right: 0px; - bottom: 0px; - left: 0px; -} - - -.upload-button { - cursor: pointer; - padding: 0.25rem; - font-size: 1rem; - border: none; - font-family: 'Oswald', sans-serif; - background-color: transparent; -} - -.upload-button:hover { - text-decoration-line: underline; -} - -/* .file-preview-container{ - display: flex; - flex-direction: row; - gap: 0.5rem; - width: 100%; - height: 10vh; -} */ \ No newline at end of file diff --git a/src/components/common/DragAndDrop/DragAndDrop.jsx b/src/components/common/DragAndDrop/DragAndDrop.jsx index e2948c09ee..645db808ea 100644 --- a/src/components/common/DragAndDrop/DragAndDrop.jsx +++ b/src/components/common/DragAndDrop/DragAndDrop.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/function-component-definition */ import { useState } from 'react'; -import './DragAndDrop.css'; +import styles from './DragAndDrop.module.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faImage } from '@fortawesome/free-solid-svg-icons'; @@ -22,46 +22,50 @@ const DragAndDrop = ({ updateUploadedFiles }) => { e.stopPropagation(); setDragActive(false); const droppedFiles = e.dataTransfer.files; - if (droppedFiles && droppedFiles[0]) { + if (droppedFiles && droppedFiles.length > 0) { const newFiles = Array.from(droppedFiles); - updateUploadedFiles(prev => [...prev, ...newFiles]); + updateUploadedFiles(newFiles); } }; const handleChange = function handleFileChange(e) { e.preventDefault(); const selectedFiles = e.target.files; - if (selectedFiles && selectedFiles[0]) { + if (selectedFiles && selectedFiles.length > 0) { const newFiles = Array.from(selectedFiles); - updateUploadedFiles(prev => [...prev, ...newFiles]); + updateUploadedFiles(newFiles); + e.target.value = ''; } }; return ( -
e.preventDefault()}> +
e.preventDefault()} + > - {/* invisible element to cover the entire form when dragActive is true so that dragleave event is not triggeredwhen drag goes over other elements in the form. */} {dragActive && (
{ `${APIEndpoint}/tools/availability?toolId=${toolId}&projectId=${projectId}`, BM_LOG_TOOLS: `${APIEndpoint}/bm/tools/log`, BM_EQUIPMENT_BY_ID: singleEquipmentId => `${APIEndpoint}/bm/equipment/${singleEquipmentId}`, + BM_EQUIPMENT_STATUS_UPDATE: (equipmentId) => `${APIEndpoint}/bm/equipment/${equipmentId}/status`, BM_EQUIPMENTS: `${APIEndpoint}/bm/equipments`, BM_INVTYPE_TYPE: type => `${APIEndpoint}/bm/invtypes/${type}`, BM_ISSUE_CHART: `${APIEndpoint}/bm/issue/issue-chart`,