diff --git a/packages/camera/package.json b/packages/camera/package.json index 6aa4c2f29..7f46f7c32 100644 --- a/packages/camera/package.json +++ b/packages/camera/package.json @@ -44,11 +44,8 @@ }, "dependencies": { "@expo/match-media": "^0.3.0", - "@expo/vector-icons": "^12.0.5", - "@unimodules/core": "^7.1.2", - "@unimodules/react-native-adapter": "^6.3.9", "axios": "^0.24.0", - "expo-image-manipulator": "^10.2.1", + "expo-image-manipulator": "~11.1.1", "expo-screen-orientation": "^4.1.1", "i18next": "^21.8.13", "image-conversion": "^2.1.1", diff --git a/packages/camera/src/components/AddDamageModal/AddDamageHelpModal.js b/packages/camera/src/components/AddDamageModal/AddDamageHelpModal.js index eac3b082b..2fcecebab 100644 --- a/packages/camera/src/components/AddDamageModal/AddDamageHelpModal.js +++ b/packages/camera/src/components/AddDamageModal/AddDamageHelpModal.js @@ -14,6 +14,7 @@ const styles = StyleSheet.create({ padding: 20, paddingBottom: 5, maxWidth: 400, + maxHeight: 235, }, header: { alignSelf: 'stretch', diff --git a/packages/camera/src/components/AddDamageModal/PartSelector.js b/packages/camera/src/components/AddDamageModal/PartSelector.js index 15c0c054b..fa68ca7a0 100644 --- a/packages/camera/src/components/AddDamageModal/PartSelector.js +++ b/packages/camera/src/components/AddDamageModal/PartSelector.js @@ -1,7 +1,7 @@ import { CarView360 } from '@monkvision/inspection-report'; import PropTypes from 'prop-types'; import React, { useCallback, useMemo } from 'react'; -import { useWindowDimensions } from 'react-native'; +import { useWindowDimensions, Platform } from 'react-native'; const PART_SELECTOR_CONTAINER_WIDTH = 420; const PART_SELECTOR_CONTAINER_HEIGHT_DIMENSION = [ @@ -10,9 +10,9 @@ const PART_SELECTOR_CONTAINER_HEIGHT_DIMENSION = [ { screenHeightSpan: [310, 99999], partSelectorHeight: 235 }, ]; -const selectedPartAttributes = { +const selectedPartAttributes = (Platform.OS === 'web') ? { style: { fill: '#ADE0FFB3' }, -}; +} : { fill: '#ADE0FFB3' }; export default function PartSelector({ orientation, togglePart, isPartSelected, vehicleType }) { const { height } = useWindowDimensions(); diff --git a/packages/camera/src/components/AddDamageModal/assets/RotateLeft.js b/packages/camera/src/components/AddDamageModal/assets/RotateLeft.js index 3ad96583d..f5007b4b1 100644 --- a/packages/camera/src/components/AddDamageModal/assets/RotateLeft.js +++ b/packages/camera/src/components/AddDamageModal/assets/RotateLeft.js @@ -1,6 +1,7 @@ import * as React from 'react'; import Svg, { Path } from 'react-native-svg'; import PropTypes from 'prop-types'; +import { Platform } from 'react-native'; export default function RotateLeft({ width, height }) { return ( @@ -11,7 +12,7 @@ export default function RotateLeft({ width, height }) { xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 21" preserveAspectRatio="xMidYMid slice" - pointerEvents="box-none" + pointerEvents={Platform.OS === 'web' ? 'box-none' : undefined} > - ); -} - -Overlay.propTypes = { - label: PropTypes.string, - svg: PropTypes.string.isRequired, -}; - -Overlay.defaultProps = { - label: '', -}; diff --git a/packages/camera/src/components/Camera/index.native.js b/packages/camera/src/components/Camera/index.native.js index b4b30573c..2290332bc 100644 --- a/packages/camera/src/components/Camera/index.native.js +++ b/packages/camera/src/components/Camera/index.native.js @@ -1,4 +1,4 @@ -import React, { forwardRef, useCallback } from 'react'; +import React, { forwardRef, useCallback, useState, useImperativeHandle } from 'react'; import PropTypes from 'prop-types'; import { I18nextProvider, useTranslation } from 'react-i18next'; @@ -22,6 +22,19 @@ function Camera({ }, ref) { const { t } = useTranslation(); const permissions = usePermissions(); + const [cameraRef, setCameraRef] = useState(null); + + useImperativeHandle(ref, () => ({ + async takePictureAsync(options) { + return cameraRef?.takePictureAsync(options); + }, + resumePreview() { + cameraRef?.resumePreview(); + }, + pausePreview() { + cameraRef?.pausePreview(); + }, + })); const { height: windowHeight, width: windowWidth } = useWindowDimensions(); const size = getSize(ratio, { windowHeight, windowWidth }); @@ -33,7 +46,9 @@ function Camera({ if (permissions.granted && permissions.status === PermissionStatus.GRANTED) { return ( { + setCameraRef(reference); + }} ratio={ratio} onMountError={handleError} {...passThroughProps} diff --git a/packages/camera/src/components/Camera/styles.js b/packages/camera/src/components/Camera/styles.js index 6a94150ca..756984178 100644 --- a/packages/camera/src/components/Camera/styles.js +++ b/packages/camera/src/components/Camera/styles.js @@ -1,4 +1,4 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Platform } from 'react-native'; export default StyleSheet.create({ container: { @@ -12,7 +12,11 @@ export default StyleSheet.create({ backgroundColor: 'rgba(0,0,0,0.75)', borderRadius: 18, color: 'white', - fontFamily: 'monospace', + ...Platform.select({ + web: { + fontFamily: 'monospace', + }, + }), fontSize: 14, lineHeight: 9, marginTop: 4, diff --git a/packages/camera/src/components/Capture/capture.js b/packages/camera/src/components/Capture/capture.js index 9bdcd5590..41f7a1888 100644 --- a/packages/camera/src/components/Capture/capture.js +++ b/packages/camera/src/components/Capture/capture.js @@ -68,14 +68,24 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(0,0,0,0.5)', }, overlayContainer: { - position: 'fixed', display: 'flex', justifyContent: 'center', alignItems: 'center', - width: '100vw', - height: '95vh', - top: '2.5vh', zIndex: 99, + ...Platform.select({ + web: { + position: 'fixed', + height: '95vh', + width: '100vw', + top: '2.5vh', + }, + native: { + position: 'static', + height: '95%', + width: '100%', + top: '2.5%', + }, + }), }, addDamageOverlay: { fontSize: 14, @@ -280,7 +290,9 @@ const Capture = forwardRef(({ endTour, }; const startUploadAsync = useStartUploadAsync(startUploadAsyncParams); - const uploadAdditionalDamage = useUploadAdditionalDamage({ inspectionId, addDamageParts }); + const uploadAdditionalDamage = useUploadAdditionalDamage({ + inspectionId, addDamageParts, onPictureUploaded, + }); const [goPrevSight, goNextSight] = useNavigationBetweenSights({ sights }); @@ -317,7 +329,8 @@ const Capture = forwardRef(({ const windowDimensions = useWindowDimensions(); const tourHasFinished = useMemo( - () => Object.values(uploads.state).every(({ status, uploadCount }) => (status === 'rejected' || status === 'fulfilled') && uploadCount >= 1), + () => Object.values(uploads.state) + .every(({ status, uploadCount }) => (status === 'rejected' || status === 'fulfilled') && uploadCount >= 1), [uploads.state], ); const overlaySize = useMemo( @@ -371,7 +384,7 @@ const Capture = forwardRef(({ const handlePartSelectorConfirm = useCallback((selectedParts) => { setAddDamageParts(selectedParts); setAddDamageStatus(AddDamageStatus.TAKE_PICTURE); - }, [setAddDamageStatus]); + }, [setAddDamageParts, setAddDamageStatus]); const handleCloseCaptureEarly = useCallback(() => { if (typeof onCloseEarly === 'function') { @@ -715,7 +728,8 @@ const Capture = forwardRef(({ {children} - {[AddDamageStatus.HELP, AddDamageStatus.PART_SELECTOR].includes(addDamageStatus) ? ( + { + [AddDamageStatus.HELP, AddDamageStatus.PART_SELECTOR].includes(addDamageStatus) ? ( {addDamageStatus === AddDamageStatus.HELP ? ( ) : null} - ) : null} + ) : null + } {closeEarlyModalState.show ? ( {} }) { const { t, i18n } = useTranslation(); return useCallback(async ({ picture, parts }) => { @@ -314,10 +335,22 @@ export function useUploadAdditionalDamage({ inspectionId, addDamageParts }) { } try { - const fileType = picture.fileType; - const filename = `close-up-${Date.now()}-${inspectionId}.${picture.imageFilenameExtension}`; + let fileType; + let filename; + let fileKey; + if (Platform.OS === 'web') { + fileType = picture.fileType; + filename = `close-up-${Date.now()}-${inspectionId}.${picture.imageFilenameExtension}`; + fileKey = 'image'; + } else { + const fileExtension = picture.uri.split(/[#?]/)[0].split('.').pop().trim(); + fileType = `image/${fileExtension}`; + filename = `close-up-${Date.now()}-${inspectionId}.${fileType}`; + fileKey = filename; + } + const multiPartKeys = { - image: 'image', + image: fileKey, json: 'json', type: fileType, filename, @@ -335,7 +368,7 @@ export function useUploadAdditionalDamage({ inspectionId, addDamageParts }) { const json = JSON.stringify({ acquisition: { strategy: 'upload_multipart_form_keys', - file_key: multiPartKeys.image, + file_key: fileKey, }, compliances: { image_quality_assessment: {}, @@ -352,30 +385,43 @@ export function useUploadAdditionalDamage({ inspectionId, addDamageParts }) { }, }); - let fileBits; - + let file; if (Platform.OS === 'web') { const res = await axios.get(picture.uri, { responseType: 'blob' }); - const file = res.data; - - fileBits = [file]; + const fileBits = [res.data]; + file = await new File( + fileBits, + multiPartKeys.filename, + { type: multiPartKeys.type }, + ); } else { - const buffer = Buffer.from(picture.uri, 'base64'); - fileBits = new Blob([buffer], { type: picture.imageFilenameExtension }); + file = { + uri: picture.uri, + type: multiPartKeys.type, + name: multiPartKeys.filename, + }; } - const file = await new File( - fileBits, - multiPartKeys.filename, - { type: multiPartKeys.type }, - ); - try { const data = new FormData(); data.append(multiPartKeys.json, json); data.append(multiPartKeys.image, file); + let result; + if (Platform.OS === 'web') { + result = await monk.entity.image.addOne(inspectionId, data); + } else { + const response = await fetch(`${monk.config.axiosConfig.baseURL}/inspections/${inspectionId}/images`, { + method: 'post', + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: monk.config.accessToken, + }, + body: data, + }); + result = await response.json(); + } - await monk.entity.image.addOne(inspectionId, data); + onPictureUploaded({ result, picture, inspectionId }); } catch (err) { console.error(err); } finally { diff --git a/packages/camera/src/components/Controls/index.js b/packages/camera/src/components/Controls/index.js index 809107242..da7777bd9 100644 --- a/packages/camera/src/components/Controls/index.js +++ b/packages/camera/src/components/Controls/index.js @@ -1,7 +1,6 @@ import React, { createElement, useCallback, useMemo, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Platform, StyleSheet, TouchableOpacity, useWindowDimensions, View } from 'react-native'; -import { isIOS } from 'react-device-detect'; import AddDamageButton from './AddDamageButton'; import CloseEarlyButton from './CloseEarlyButton'; @@ -25,7 +24,7 @@ const styles = StyleSheet.create({ display: 'flex', flexDirection: 'column', alignItems: 'center', - justifyContent: 'end', + justifyContent: 'flex-end', }, controlArraySpacer: { height: 20, @@ -364,10 +363,9 @@ Controls.getFullScreenButtonProps = (isFullscreen) => ({ shadowColor: '#181829', shadowOpacity: 0.5, shadowOffset: { width: 0, height: 0 }, - visibility: isIOS ? 'hidden' : 'visible', ...Platform.select({ - native: { shadowRadius: 2 }, - default: { shadowRadius: '2px 2px' }, + native: { shadowRadius: 2, opacity: 0 }, + default: { shadowRadius: '2px 2px', visibility: 'visible' }, }), }, }); diff --git a/packages/camera/src/components/Layout/index.native.js b/packages/camera/src/components/Layout/index.native.js index 888b98c38..3b12b1da5 100644 --- a/packages/camera/src/components/Layout/index.native.js +++ b/packages/camera/src/components/Layout/index.native.js @@ -1,11 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, useWindowDimensions, View } from 'react-native'; +import { StyleSheet, useWindowDimensions, View, Text } from 'react-native'; +import { useTranslation } from 'react-i18next'; const SIDE = 116; export const SIDE_WIDTH = SIDE; const styles = StyleSheet.create({ + rotate: { + justifyContent: 'center', + alignItems: 'center', + }, container: { flexDirection: 'row', justifyContent: 'space-between', @@ -25,10 +30,20 @@ const styles = StyleSheet.create({ display: 'flex', justifyContent: 'center', }, + title: { + color: 'rgba(250, 250, 250, 0.87)', + textAlign: 'center', + fontWeight: '500', + lineHeight: 30, + letterSpacing: 0.15, + fontSize: 20, + marginVertical: 2, + }, }); function Layout({ backgroundColor, children, isReady, left, right }) { const { height, width } = useWindowDimensions(); + const { t } = useTranslation(); const size = StyleSheet.create({ height, width }); @@ -42,25 +57,40 @@ function Layout({ backgroundColor, children, isReady, left, right }) { styles.rightPortrait, ); + if (width > height) { + return ( + + {isReady && left} + + {children} + + {isReady && right} + + ); + } return ( - {isReady && left} - - {children} - - {isReady && right} + + {t('layout.rotateDevice')} + ); } diff --git a/packages/camera/src/components/Overlay/index.native.js b/packages/camera/src/components/Overlay/index.native.js index 1c6cb31d9..1ef960ba6 100644 --- a/packages/camera/src/components/Overlay/index.native.js +++ b/packages/camera/src/components/Overlay/index.native.js @@ -1,11 +1,25 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { SvgXml } from 'react-native-svg'; +import { SvgCss } from 'react-native-svg'; export default function Overlay({ label, svg, ...passThoughProps }) { + let newSVG = svg; + if (!svg.includes('viewBox')) { + const widthParam = 'width="'; + const widthStart = svg.indexOf(widthParam) + widthParam.length; + const widthEnd = widthStart + svg.substring(widthStart).indexOf('"'); + const width = svg.substring(widthStart, widthEnd); + + const heighParam = 'height="'; + const heightStart = svg.indexOf(heighParam) + heighParam.length; + const heightEnd = heightStart + svg.substring(heightStart).indexOf('"'); + const height = svg.substring(heightStart, heightEnd); + + newSVG = svg.replace(svg.substring(svg.indexOf(widthParam), heightEnd + 1), `x="0" y="0" viewBox="0 0 ${width} ${height}"`); + } return ( - `${EVENTS_PREFIX}${key}`, [key]); useEffect(() => { - const timestampStored = localStorage.getItem(storeKey); + let timestampStored; + if (Platform.OS === 'web') { + timestampStored = localStorage.getItem(storeKey); + } else { + timestampStored = savedEvents.storeKey; + } if (timestampStored) { setLastEventTimestamp(timestampStored); } else { @@ -25,7 +32,11 @@ export default function useEventStorage({ const fireEvent = useCallback(() => { const timestamp = Date.now(); - localStorage.setItem(storeKey, timestamp.toString()); + if (Platform.OS === 'web') { + localStorage.setItem(storeKey, timestamp.toString()); + } else { + savedEvents.storeKey = timestamp.toString(); + } setLastEventTimestamp(timestamp); }, [storeKey, setLastEventTimestamp]); diff --git a/packages/inspection-report/src/components/CarView360/CarView360Handles.js b/packages/inspection-report/src/components/CarView360/CarView360Handles.js index 95a6fca7f..57e5c5312 100644 --- a/packages/inspection-report/src/components/CarView360/CarView360Handles.js +++ b/packages/inspection-report/src/components/CarView360/CarView360Handles.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, View, Platform } from 'react-native'; import { IconChevronLeft, IconChevronRight } from '../../assets'; import { CarOrientation, CommonPropTypes } from '../../resources'; @@ -35,7 +35,10 @@ const styles = StyleSheet.create({ cursor: 'pointer', }, selectedDot: { - transform: { scale: 1.5 }, + ...Platform.select({ + web: { transform: { scale: 1.5 } }, + native: { transform: [{ scale: 1.5 }] }, + }), backgroundColor: '#FFFFFF', }, }); diff --git a/packages/inspection-report/src/components/CarView360/hooks/useCustomSVGAttributes.js b/packages/inspection-report/src/components/CarView360/hooks/useCustomSVGAttributes.js index 069605493..af684e298 100644 --- a/packages/inspection-report/src/components/CarView360/hooks/useCustomSVGAttributes.js +++ b/packages/inspection-report/src/components/CarView360/hooks/useCustomSVGAttributes.js @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { Platform } from 'react-native'; import { CarParts } from '../../../resources'; import { getPillDamage, isPricingPill } from './common'; @@ -53,7 +54,7 @@ function getCustomPillChildAttributes({ damages, groupId, elementClass, onPressP const onClick = () => onPressPill(part); return { - pointerEvents: 'all', + pointerEvents: Platform.OS === 'web' ? 'all' : 'box-only', style: styles.pillChild, onClick, }; @@ -107,7 +108,7 @@ export default function useCustomSVGAttributes({ const onClick = () => onPressPart(part); return { - pointerEvents: 'all', + pointerEvents: Platform.OS === 'web' ? 'all' : 'box-only', onClick, ...partAttributes, }; diff --git a/packages/inspection-report/src/components/CarView360/hooks/useXMLParser.js b/packages/inspection-report/src/components/CarView360/hooks/useXMLParser.js index 9e4bef1a0..f8a7bcec6 100644 --- a/packages/inspection-report/src/components/CarView360/hooks/useXMLParser.js +++ b/packages/inspection-report/src/components/CarView360/hooks/useXMLParser.js @@ -1,5 +1,7 @@ import { useMemo } from 'react'; +const { DOMParser } = require('xmldom'); + export default function useXMLParser(xml) { return useMemo(() => new DOMParser().parseFromString(xml, 'text/xml'), [xml]); } diff --git a/packages/inspection-report/src/components/CarView360/index.js b/packages/inspection-report/src/components/CarView360/index.js index 78cad12d5..60ad5810c 100644 --- a/packages/inspection-report/src/components/CarView360/index.js +++ b/packages/inspection-report/src/components/CarView360/index.js @@ -1,11 +1,11 @@ import PropTypes from 'prop-types'; import React, { useMemo } from 'react'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet, View, Platform } from 'react-native'; import { CarOrientation, RepairOperation, VehicleType } from '../../resources'; import { useOrientation, ORIENTATION_MODE } from '../../hooks'; import { useCarView360Wireframe, useXMLParser } from './hooks'; -import SVGElementMapper from './SVGElementMapper'; +import SVGElementMapper from './svgElementMapper'; const styles = StyleSheet.create({ container: { @@ -26,13 +26,50 @@ export default function CarView360({ onPressPart, onPressPill, }) { - const wireframeXML = useCarView360Wireframe({ orientation, vehicleType }); + let wireframeXML = useCarView360Wireframe({ orientation, vehicleType }); + if (Platform.OS !== 'web') { + const svgStyles = wireframeXML.substring(wireframeXML.indexOf('')); + const elements = svgStyles.split('.'); + const styleDict = {}; + let classNames = []; + elements.forEach((element) => { + if (element.includes('{')) { + const style = element.substring(element.indexOf('{') + 1, element.indexOf('}')); + classNames.push(element.substring(0, element.indexOf('{')).replace(/[,.]/g, '')); + classNames.forEach((name) => { + const prev = styleDict[name] ? `${styleDict[name]};` : ''; + styleDict[name] = `${prev}${style};`; + }); + classNames = []; + } else { + classNames.push(element.replace(/[,.]/g, '')); + } + }); + + Object.keys(styleDict).forEach((key) => { + const newValue = styleDict[key].split(';').filter((value) => value.length !== 0).map((element) => { + const separatorIndex = element.indexOf(':'); + const value = element.substring(separatorIndex + 1, element.length); + const name = element.substring(0, separatorIndex); + return `${name} = "${value}"`; + }).reduce( + (accumulator, currentValue) => `${accumulator} ${currentValue}`, + '', + ); + wireframeXML = wireframeXML.replaceAll(`class="${key}`, `${newValue} class="${key}`); + }); + } const windowOrientation = useOrientation(); const doc = useXMLParser(wireframeXML); const svgElement = useMemo(() => { - const svg = doc.children[0]; + let svg; + if (Platform.OS === 'web') { + svg = doc.children[0]; + } else { + svg = doc.childNodes[1] ?? doc.childNodes[0]; + } if (svg.tagName !== 'svg') { - throw new Error('Invalid Part View 360 SVG: expected tag as the first children of XML document.'); + throw new Error('Invalid part selector SVG format: expected tag as the first children of XML document'); } return svg; }, [doc]); diff --git a/packages/inspection-report/src/components/CarView360/SVGElementMapper.js b/packages/inspection-report/src/components/CarView360/svgElementMapper/index.js similarity index 95% rename from packages/inspection-report/src/components/CarView360/SVGElementMapper.js rename to packages/inspection-report/src/components/CarView360/svgElementMapper/index.js index 372db4fc0..b375b96d7 100644 --- a/packages/inspection-report/src/components/CarView360/SVGElementMapper.js +++ b/packages/inspection-report/src/components/CarView360/svgElementMapper/index.js @@ -1,9 +1,9 @@ /* eslint-disable react/no-array-index-key */ import PropTypes from 'prop-types'; import React, { useMemo } from 'react'; -import { CommonPropTypes } from '../../resources'; +import { CommonPropTypes } from '../../../resources'; -import { useCustomSVGAttributes, useInnerHTML, useJSXSpecialAttributes } from './hooks'; +import { useCustomSVGAttributes, useInnerHTML, useJSXSpecialAttributes } from '../hooks'; export default function SVGElementMapper({ element, diff --git a/packages/inspection-report/src/components/CarView360/svgElementMapper/index.native.js b/packages/inspection-report/src/components/CarView360/svgElementMapper/index.native.js new file mode 100644 index 000000000..9934b3d2f --- /dev/null +++ b/packages/inspection-report/src/components/CarView360/svgElementMapper/index.native.js @@ -0,0 +1,167 @@ +/* eslint-disable react/no-array-index-key */ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import Svg, { Path, G, Circle, Text } from 'react-native-svg'; + +import { CommonPropTypes } from '../../../resources'; +import CAR_PARTS from '../../../resources/carParts'; +import { useCustomSVGAttributes, useInnerHTML } from '../hooks'; +import { getPillDamage } from '../hooks/common'; + +const jsxSpecialAttributes = { + class: 'className', +}; + +export default function SVGElementMapper({ + element, + damages, + groupId, + getPartAttributes, + onPressPart, + onPressPill, +}) { + function getAttribute(attributeElement, name) { + for (let i = 0; i < attributeElement.attributes.length; i += 1) { + if (attributeElement.attributes[i].name === name) { + return attributeElement.attributes[i].nodeValue; + } + } + return undefined; + } + const names = []; + for (let i = 0; i < element.attributes.length; i += 1) { + names.push(element.attributes[i].name); + } + + const attributes = useMemo(() => names.reduce((prev, attr) => ({ + ...prev, + [jsxSpecialAttributes[attr] ?? attr]: getAttribute(element, attr), + }), {}), [element]); + + const elementClass = getAttribute(element, 'class'); + const elementId = getAttribute(element, 'id'); + let partKey = null; + if (groupId && CAR_PARTS.includes(groupId)) { + partKey = groupId; + } + if (elementClass && elementClass.includes('selectable') && CAR_PARTS.includes(elementId)) { + partKey = elementId; + } + + const customAttributes = useCustomSVGAttributes({ + element, + groupId, + damages, + getPartAttributes, + onPressPart, + onPressPill, + }); + const innerHTML = useInnerHTML({ element, damages, groupId }); + const { part } = getPillDamage({ damages, pillId: groupId }); + const isPill = elementClass?.includes('damage-pill'); + + const onPress = () => { + if (isPill) { + onPressPill(part); + } else { + onPressPart(partKey); + } + }; + const elementChildren = []; + if (element.childNodes) { + for (let i = 0; i < element.childNodes.length; i += 1) { + elementChildren.push(element.childNodes[i]); + } + } + const children = useMemo(() => [...elementChildren], [element]); + const passThroughGroupId = useMemo( + () => { + if (element.tagName === 'g') { + return elementId; + } + return null; + }, + [element], + ); + + if (element.tagName === 'svg') { + return ( + + {children.map((child, id) => ( + + ))} + + + ); + } if (element.tagName === 'path') { + return ( + + {children.map((child, id) => ( + + ))} + + + ); + } if (element.tagName === 'g') { + return ( + + {children.map((child, id) => ( + + ))} + + + ); + } if (element.tagName === 'circle') { + return ( + + ); + } if (element.tagName === 'text') { + return ( + + {innerHTML} + + ); + } + return null; +} + +SVGElementMapper.propTypes = { + damages: PropTypes.arrayOf(CommonPropTypes.damage), + element: PropTypes.any.isRequired, + getPartAttributes: PropTypes.func, + groupId: PropTypes.string, + onPressPart: PropTypes.func, + onPressPill: PropTypes.func, +}; + +SVGElementMapper.defaultProps = { + damages: [], + getPartAttributes: () => {}, + groupId: null, + onPressPart: () => {}, + onPressPill: () => {}, +}; diff --git a/packages/inspection-report/src/components/DamageReport/Accordion.js b/packages/inspection-report/src/components/DamageReport/Accordion.js index 2d38d4856..98344731e 100644 --- a/packages/inspection-report/src/components/DamageReport/Accordion.js +++ b/packages/inspection-report/src/components/DamageReport/Accordion.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, View, Platform } from 'react-native'; import { IconButton } from '../common'; const styles = StyleSheet.create({ @@ -13,12 +13,19 @@ const styles = StyleSheet.create({ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', - padding: '10px', + ...Platform.select({ + web: { + padding: '10px', + }, + native: { + pospaddingition: 10, + }, + }), }, title: { color: '#fafafa', fontSize: 18, - } + }, }); function Accordion({ title, isCollapsed, children, onPress }) { @@ -29,7 +36,7 @@ function Accordion({ title, isCollapsed, children, onPress }) { {title} @@ -42,10 +49,10 @@ function Accordion({ title, isCollapsed, children, onPress }) { } Accordion.propTypes = { - title: PropTypes.string, - isCollapsed: PropTypes.bool, children: PropTypes.element, + isCollapsed: PropTypes.bool, onPress: PropTypes.func, + title: PropTypes.string, }; Accordion.defaultProps = { diff --git a/packages/inspection-report/src/components/DamageReport/DamageManipulator.js b/packages/inspection-report/src/components/DamageReport/DamageManipulator.js index e5d0e4e4f..2dbb3e692 100644 --- a/packages/inspection-report/src/components/DamageReport/DamageManipulator.js +++ b/packages/inspection-report/src/components/DamageReport/DamageManipulator.js @@ -3,7 +3,7 @@ import Slider from '@react-native-community/slider'; import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, View, Platform } from 'react-native'; import { IconSeverity, SeveritiesWithIcon } from '../../assets'; import { CommonPropTypes, DamageMode, DisplayMode, Severity, RepairOperation } from '../../resources'; import { TextButton, SwitchButton } from '../common'; @@ -159,9 +159,13 @@ export default function DamageManipulator({ - + {t('damageManipulator.damages')} {t(`damageManipulator.${isReplaced ? 'replaced' : 'notReplaced'}`)} @@ -225,7 +229,10 @@ export default function DamageManipulator({ {isEditable && !isReplaced ? ( ) : ( - - {formateValue(editedDamage?.pricing ?? 0)} - + + + {formateValue(editedDamage?.pricing ?? 0)} + + )} ) diff --git a/packages/inspection-report/src/components/DamageReport/DamageReport.js b/packages/inspection-report/src/components/DamageReport/DamageReport.js index 65701c4c3..881dacf88 100644 --- a/packages/inspection-report/src/components/DamageReport/DamageReport.js +++ b/packages/inspection-report/src/components/DamageReport/DamageReport.js @@ -1,8 +1,8 @@ import { Loader } from '@monkvision/ui'; import PropTypes from 'prop-types'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, View, Platform } from 'react-native'; import { CommonPropTypes, DamageMode, VehicleType } from '../../resources'; import { IconButton } from '../common'; @@ -109,7 +109,14 @@ const styles = StyleSheet.create({ marginTop: 15, }, galleryWrapper: { - maxHeight: '39vh', + ...Platform.select({ + web: { + maxHeight: '39vh', + }, + native: { + maxHeight: '39%', + }, + }), overflowY: 'auto', paddingBottom: 15, }, @@ -128,6 +135,7 @@ export default function DamageReport({ generatePdf, pdfOptions, onStartNewInspection, + onPdfPressed, }) { const { t } = useTranslation(); const { updateCurrency } = useCurrency(); @@ -178,6 +186,7 @@ export default function DamageReport({ }); const { + reportUrl, pdfStatus, requestPdf, handleDownload, @@ -189,6 +198,11 @@ export default function DamageReport({ clientName: pdfOptions?.clientName, }); + const handlePDFDownload = useCallback(() => { + handleDownload(); + onPdfPressed(reportUrl); + }); + const { confirmModal, handleHideConfirmModal, @@ -254,7 +268,7 @@ export default function DamageReport({ )} @@ -377,7 +391,7 @@ export default function DamageReport({ part={editedDamagePart} damage={editedDamage} damageMode={damageMode} - imageCount={editedDamageImages.length} + imageCount={(editedDamageImages ?? []).length} onDismiss={handlePopUpDismiss} onShowGallery={handleShowGallery} onConfirm={handleSaveDamage} @@ -398,13 +412,15 @@ export default function DamageReport({ /> ) } - {confirmModal && ( - - )} + { + confirmModal && ( + + ) + } ); } @@ -414,6 +430,7 @@ DamageReport.propTypes = { damageMode: CommonPropTypes.damageMode, generatePdf: PropTypes.bool, inspectionId: PropTypes.string.isRequired, + onPdfPressed: PropTypes.func, onStartNewInspection: PropTypes.func, pdfOptions: PropTypes.shape({ clientName: PropTypes.string.isRequired, @@ -427,6 +444,7 @@ DamageReport.defaultProps = { damageMode: DamageMode.ALL, generatePdf: false, onStartNewInspection: () => { }, + onPdfPressed: () => { }, pdfOptions: undefined, vehicleType: VehicleType.CUV, }; diff --git a/packages/inspection-report/src/components/DamageReport/Overview/DamageCounts.js b/packages/inspection-report/src/components/DamageReport/Overview/DamageCounts.js index ce5aa5b77..5c7c7fe9f 100644 --- a/packages/inspection-report/src/components/DamageReport/Overview/DamageCounts.js +++ b/packages/inspection-report/src/components/DamageReport/Overview/DamageCounts.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, View, Platform } from 'react-native'; import { IconSeverityNone, SeveritiesWithIcon } from '../../../assets'; import { CommonPropTypes, DamageMode } from '../../../resources'; @@ -31,7 +31,10 @@ const styles = StyleSheet.create({ paddingLeft: 10, }, severityCount: { - fontWeight: 'medium', + ...Platform.select({ + web: { fontWeight: 'medium' }, + native: { fontWeight: 'normal' }, + }), fontSize: 12, color: '#FFFFFF', }, diff --git a/packages/inspection-report/src/components/DamageReport/Overview/index.js b/packages/inspection-report/src/components/DamageReport/Overview/index.js index fd77e6edd..e5be66994 100644 --- a/packages/inspection-report/src/components/DamageReport/Overview/index.js +++ b/packages/inspection-report/src/components/DamageReport/Overview/index.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View, Platform } from 'react-native'; import { CarOrientation, CommonPropTypes, DamageMode, VehicleType } from '../../../resources'; import CarView360 from '../../CarView360'; @@ -25,6 +25,11 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + overviewViewContainer: { + paddingVertical: 20, + alignItems: 'center', + justifyContent: 'space-evenly', + }, buttonsContainer: { marginTop: 20, alignSelf: 'stretch', @@ -67,7 +72,7 @@ export default function Overview({ pdfHandles: { pdfStatus, handleDownload }, onStartNewInspection, }) { - const { width } = useWindowDimensions(); + const { width, height } = useWindowDimensions(); const { orientation, rotateLeft, @@ -101,7 +106,12 @@ export default function Overview({ - + { - images.map((image, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - - )) - } + images.map((image, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + )) + } {images.length === 0 ? 0 : (currentPhotoIndex + 1)} diff --git a/packages/inspection-report/src/components/DamageReport/UpdateDamagePopUp/index.js b/packages/inspection-report/src/components/DamageReport/UpdateDamagePopUp/index.js index d81dd375d..ee6cbaaed 100644 --- a/packages/inspection-report/src/components/DamageReport/UpdateDamagePopUp/index.js +++ b/packages/inspection-report/src/components/DamageReport/UpdateDamagePopUp/index.js @@ -7,7 +7,9 @@ import { PanResponder, StyleSheet, TouchableWithoutFeedback, - useWindowDimensions, Platform, + useWindowDimensions, + Platform, + ScrollView, } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useOrientation, useDesktopMode } from '../../../hooks'; @@ -37,10 +39,16 @@ const styles = StyleSheet.create({ borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingTop: 6, - paddingBottom: 6, + ...Platform.select({ + web: { paddingBottom: 6 }, + native: { paddingBottom: 100 }, + }), paddingLeft: 16, paddingRight: 16, - top: 650, + ...Platform.select({ + web: { top: 650 }, + native: { top: 50 }, + }), }, horizontalBarContent: { display: 'flex', @@ -57,7 +65,9 @@ const styles = StyleSheet.create({ flex: 1, alignSelf: 'center', width: '100%', - maxWidth: '500px', + ...Platform.select({ + web: { maxWidth: '500px' }, + }), position: 'relative', overflowY: 'auto', }, @@ -108,13 +118,14 @@ export default function UpdateDamagePopUp({ const [viewMode, setViewMode] = useState(null); const [gestureState, setGestureState] = useState({}); const pan = useRef(new Animated.ValueXY({ x: 0, y: bottomLimitY })).current; + const topLimitY = Platform.OS === 'web' ? topLimitY : 0; const handleToggleDamage = useCallback((isToggled) => { setViewMode(isToggled ? DisplayMode.FULL : DisplayMode.MINIMAL); }, []); const scrollIn = useCallback(() => { - const toValue = viewMode === DisplayMode.FULL ? topLimitY : bottomLimitY / 1.8; + const toValue = viewMode === DisplayMode.FULL ? topLimitY : bottomLimitY / (Platform.OS === 'web' ? 1.8 : 3.5); Animated.timing(pan, { toValue: { x: 0, y: toValue }, duration: 200, @@ -160,7 +171,11 @@ export default function UpdateDamagePopUp({ pan.setValue({ x: 0, y: bottomLimitY }); } else { Animated.event( - [null, { moveX: pan.x, moveY: pan.y }], + [{ moveX: pan.x, moveY: pan.y }, { + nativeEvent: { + contentOffset: { y: pan.y, x: pan.x }, + }, + }], { useNativeDriver: Platform.OS !== 'web' }, )(event, gestureStat); } @@ -172,7 +187,7 @@ export default function UpdateDamagePopUp({ ).current; const topOffset = useMemo( - () => (viewMode === DisplayMode.FULL ? topLimitY : bottomLimitY / 1.8), + () => (viewMode === DisplayMode.FULL ? topLimitY : bottomLimitY / (Platform.OS === 'web' ? 1.8 : 3.5)), [viewMode, bottomLimitY], ); @@ -188,38 +203,61 @@ export default function UpdateDamagePopUp({ setViewMode(displayMode); }, [displayMode]); + const getDamageManipulator = useCallback(() => ( + + + {t(`damageReport.parts.${part}`)} + { + !isDesktopMode && ( + + ) + } + + + + ), [topOffset, + imageCount, + damage, + damageMode, + viewMode, + isEditable, + onShowGallery, + handleConfirm, + handleToggleDamage]); + return ( - + - - - - {t(`damageReport.parts.${part}`)} - { - !isDesktopMode && ( - - ) - } - - - + { + Platform.OS === 'web' ? getDamageManipulator() : {getDamageManipulator()} + } ); diff --git a/packages/inspection-report/src/components/DamageReport/hooks/usePdfReport.js b/packages/inspection-report/src/components/DamageReport/hooks/usePdfReport.js index 4a5e48e10..09d30fe03 100644 --- a/packages/inspection-report/src/components/DamageReport/hooks/usePdfReport.js +++ b/packages/inspection-report/src/components/DamageReport/hooks/usePdfReport.js @@ -100,9 +100,10 @@ export default function usePdfReport({ download(reportUrl, inspectionId).catch((err) => { console.error('Error while downloading the PDF :', err); }); - }, [reportUrl, inspectionId]); + }, [pdfStatus, reportUrl, inspectionId]); return { + reportUrl, pdfStatus, requestPdf, handleDownload, diff --git a/packages/inspection-report/src/components/Gallery/Thumbnail.js b/packages/inspection-report/src/components/Gallery/Thumbnail.js index e8cec77ff..52a08be11 100644 --- a/packages/inspection-report/src/components/Gallery/Thumbnail.js +++ b/packages/inspection-report/src/components/Gallery/Thumbnail.js @@ -58,7 +58,9 @@ function Thumbnail({ image, click }) { ) } - {label} {image?.isRendered && t('gallery.withDamages')} + {label} + {' '} + {image?.isRendered && t('gallery.withDamages')} @@ -68,12 +70,12 @@ function Thumbnail({ image, click }) { Thumbnail.propTypes = { click: PropTypes.func, image: PropTypes.shape({ + isRendered: PropTypes.bool, label: PropTypes.shape({ en: PropTypes.string, fr: PropTypes.string, }), url: PropTypes.string, - isRendered: PropTypes.bool, }), }; @@ -85,7 +87,7 @@ Thumbnail.defaultProps = { fr: '', }, url: '', - isRendered: false + isRendered: false, }, }; diff --git a/packages/inspection-report/src/components/Gallery/index.js b/packages/inspection-report/src/components/Gallery/index.js index 35253d87e..e1f6083ec 100644 --- a/packages/inspection-report/src/components/Gallery/index.js +++ b/packages/inspection-report/src/components/Gallery/index.js @@ -11,6 +11,7 @@ import { Text, useWindowDimensions, View, + ScrollView, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; @@ -21,8 +22,12 @@ import { useDesktopMode, useDamageVisibility } from '../../hooks'; const styles = StyleSheet.create({ container: { alignContent: 'flex-start', - flex: 1, - flexDirection: 'row', + ...Platform.select({ + web: { + flexDirection: 'row', + flex: 1, + }, + }), flexWrap: 'wrap', justifyContent: 'center', paddingVertical: 15, @@ -62,6 +67,9 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', zIndex: 99, + ...Platform.select({ + native: { paddingTop: 50 }, + }), }, title: { fontSize: 24, @@ -75,6 +83,9 @@ const styles = StyleSheet.create({ top: 20, left: 20, zIndex: 999, + ...Platform.select({ + native: { paddingTop: 50 }, + }), }, partsImageWrapper: { borderColor: '#a29e9e', @@ -162,7 +173,7 @@ function Gallery({ pictures }) { }, [gestureState]); useEffect(() => { - if (focusedPhoto) { + if (focusedPhoto && Platform.OS === 'web') { const handleKeyboardChange = (event) => { if (event.defaultPrevented) { return; // Do nothing if the event was already processed @@ -196,27 +207,51 @@ function Gallery({ pictures }) { return () => { }; }, [pictures, focusedPhoto]); + const renderList = useCallback(() => { + if (Platform.OS === 'web') { + return ( + pictures.map((image, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + + { + isDesktopMode && image?.rendered_outputs && image?.rendered_outputs?.url > 0 + && ( + + + + ) + } + + )) + ); + } + return ( + + { + pictures.map((image, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + + + )) + } + + ); + }, [pictures]); + return ( - {pictures.length > 0 ? pictures.map((image, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - - - { - isDesktopMode && image?.rendered_outputs && image?.rendered_outputs?.url && ( - - - - ) - } - - )) : ({t('gallery.empty')})} + {pictures.length > 0 ? renderList() : ({t('gallery.empty')})} { }, - updateCurrency: () => { } + updateCurrency: () => { }, }); /** @@ -27,14 +27,13 @@ export function CurrencyProvider({ children }) { const formateValue = useCallback((value) => { if (signAheadCurrencies.includes(currency)) { return `${currency}${value}`; - } else { - return `${value}${currency}`; } + return `${value}${currency}`; }, [currency]); - const currencyContextValue = useMemo(() => - ({ updateCurrency: setCurrency, formateValue }), - [currency] + const currencyContextValue = useMemo( + () => ({ updateCurrency: setCurrency, formateValue }), + [currency], ); return ( diff --git a/yarn.lock b/yarn.lock index d062eb7a5..a20280cda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7399,17 +7399,10 @@ expo-image-loader@~3.1.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-3.1.1.tgz#f88d94e66c5a102f15d858973c03b92e10662575" integrity sha512-ZX4Bh3K4CCX1aZflnmbOgFNLS+c0/GUys4wdvqxO+4A4KU1NNb3jE7RVa/OFYNPDcGhEw20c1QjyE/WsVURJpg== -expo-image-loader@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-3.2.0.tgz#d98b021660edef7243f7c5ec011b8d0545626d41" - integrity sha512-LU3Q2prn64/HxdToDmxgMIRXS1ZvD9Q3iCxRVTZn1fPQNNDciIQFE5okaa74Ogx20DFHs90r6WoUd7w9Af1OGQ== - -expo-image-manipulator@^10.2.1: - version "10.4.0" - resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-10.4.0.tgz#23570d9ca1625b9cb2583f2808172dfa088cea56" - integrity sha512-10L6eEbGGmgkZnt6bS+TkPAEuhkWa3AAlXeozLK7fKg24AUZj33FQuqc59i7ka3qMVEnsIc5bABcjimHEA4/Hg== - dependencies: - expo-image-loader "~3.2.0" +expo-image-loader@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.1.1.tgz#efadbb17de1861106864820194900f336dd641b6" + integrity sha512-ciEHVokU0f6w0eTxdRxLCio6tskMsjxWIoV92+/ZD37qePUJYMfEphPhu1sruyvMBNR8/j5iyOvPFVGTfO8oxA== expo-image-manipulator@~10.2.0: version "10.2.1" @@ -7418,6 +7411,13 @@ expo-image-manipulator@~10.2.0: dependencies: expo-image-loader "~3.1.0" +expo-image-manipulator@~11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.1.1.tgz#bb54df80e98abc9798876e3f70596a5b880168c9" + integrity sha512-W9LfJK/IL7EhhkkC1JQnEX/1S9B09rcGasJiQjXc2s1bEsrQnqXvXEv7shUW8b/L8rE+ynf+XvvDE+YIDL7oFg== + dependencies: + expo-image-loader "~4.1.0" + expo-json-utils@~0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.2.1.tgz#a8d15181e361f3fc782d5241ce381851bab4465b"