diff --git a/apps/demo-app/src/components/AppRouter.tsx b/apps/demo-app/src/components/AppRouter.tsx index d04c57069..d641ac0bc 100644 --- a/apps/demo-app/src/components/AppRouter.tsx +++ b/apps/demo-app/src/components/AppRouter.tsx @@ -8,6 +8,8 @@ import { VehicleTypeSelectionPage, } from '../pages'; import { App } from './App'; +import { CaptureSelectionPage } from '../pages/CaptureSelectionPage'; +import { DamageDisclosurePage } from '../pages/DamageDisclosurePage'; export function AppRouter() { return ( @@ -34,6 +36,15 @@ export function AppRouter() { } index /> + + + + } + index + /> + + + + } + index + /> } /> diff --git a/apps/demo-app/src/local-config.json b/apps/demo-app/src/local-config.json index c3033a559..4ca0c124f 100644 --- a/apps/demo-app/src/local-config.json +++ b/apps/demo-app/src/local-config.json @@ -3,7 +3,7 @@ "description": "Config for the local Demo App.", "workflow": "photo", "allowSkipRetake": true, - "enableAddDamage": true, + "addDamage": "part_select", "enableSightGuidelines": true, "allowVehicleTypeSelection": true, "allowManualLogin": true, @@ -15,6 +15,7 @@ "apiDomain": "api.preview.monk.ai/v1", "thumbnailDomain": "europe-west1-monk-preview-321715.cloudfunctions.net/image_resize", "enableSightTutorial": false, + "enableTutorial": "first_time_only", "startTasksOnComplete": true, "showCloseButton": false, "enforceOrientation": "landscape", diff --git a/apps/demo-app/src/pages/CaptureSelectionPage/CaptureSelectionPage.module.css b/apps/demo-app/src/pages/CaptureSelectionPage/CaptureSelectionPage.module.css new file mode 100644 index 000000000..e69de29bb diff --git a/apps/demo-app/src/pages/CaptureSelectionPage/CaptureSelectionPage.tsx b/apps/demo-app/src/pages/CaptureSelectionPage/CaptureSelectionPage.tsx new file mode 100644 index 000000000..88243c295 --- /dev/null +++ b/apps/demo-app/src/pages/CaptureSelectionPage/CaptureSelectionPage.tsx @@ -0,0 +1,17 @@ +import { CaptureSelection } from '@monkvision/common-ui-web'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Page } from '../pages'; + +export function CaptureSelectionPage() { + const navigate = useNavigate(); + const { i18n } = useTranslation(); + + return ( + navigate(Page.PHOTO_CAPTURE)} + onAddDamage={() => navigate(Page.DAMAGE_DISCLOSURE)} + /> + ); +} diff --git a/apps/demo-app/src/pages/CaptureSelectionPage/index.ts b/apps/demo-app/src/pages/CaptureSelectionPage/index.ts new file mode 100644 index 000000000..cda9efddf --- /dev/null +++ b/apps/demo-app/src/pages/CaptureSelectionPage/index.ts @@ -0,0 +1 @@ +export * from './CaptureSelectionPage'; diff --git a/apps/demo-app/src/pages/DamageDisclosurePage/DamageDisclosurePage.module.css b/apps/demo-app/src/pages/DamageDisclosurePage/DamageDisclosurePage.module.css new file mode 100644 index 000000000..fadb213e9 --- /dev/null +++ b/apps/demo-app/src/pages/DamageDisclosurePage/DamageDisclosurePage.module.css @@ -0,0 +1,7 @@ +.container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/apps/demo-app/src/pages/DamageDisclosurePage/DamageDisclosurePage.tsx b/apps/demo-app/src/pages/DamageDisclosurePage/DamageDisclosurePage.tsx new file mode 100644 index 000000000..327ac2e3b --- /dev/null +++ b/apps/demo-app/src/pages/DamageDisclosurePage/DamageDisclosurePage.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next'; +import { useMonkAppState } from '@monkvision/common'; +import { DamageDisclosure } from '@monkvision/inspection-capture-web'; +import { useNavigate } from 'react-router-dom'; +import { CaptureWorkflow, VehicleType } from '@monkvision/types'; +import styles from './DamageDisclosurePage.module.css'; +import { Page } from '../pages'; + +export function DamageDisclosurePage() { + const navigate = useNavigate(); + const { i18n } = useTranslation(); + const { config, authToken, inspectionId, vehicleType } = useMonkAppState({ + requireInspection: true, + requireWorkflow: CaptureWorkflow.PHOTO, + }); + + return ( +
+ navigate(Page.PHOTO_CAPTURE)} + lang={i18n.language} + vehicleType={vehicleType ?? VehicleType.SEDAN} + /> +
+ ); +} diff --git a/apps/demo-app/src/pages/DamageDisclosurePage/index.ts b/apps/demo-app/src/pages/DamageDisclosurePage/index.ts new file mode 100644 index 000000000..c5b4b6d1e --- /dev/null +++ b/apps/demo-app/src/pages/DamageDisclosurePage/index.ts @@ -0,0 +1 @@ +export * from './DamageDisclosurePage'; diff --git a/apps/demo-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx b/apps/demo-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx index 10248471e..244df29b5 100644 --- a/apps/demo-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx +++ b/apps/demo-app/src/pages/PhotoCapturePage/PhotoCapturePage.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useMonkAppState } from '@monkvision/common'; import { PhotoCapture } from '@monkvision/inspection-capture-web'; -import { CaptureWorkflow } from '@monkvision/types'; +import { CaptureWorkflow, VehicleType } from '@monkvision/types'; import styles from './PhotoCapturePage.module.css'; import { createInspectionReportLink } from './inspectionReport'; @@ -36,6 +36,7 @@ export function PhotoCapturePage() { sights={currentSights} onComplete={handleComplete} lang={i18n.language} + vehicleType={vehicleType ?? VehicleType.SEDAN} /> ); diff --git a/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx b/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx index bd8152e00..dafe44ba3 100644 --- a/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx +++ b/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx @@ -12,7 +12,7 @@ export function VehicleTypeSelectionPage() { const { i18n } = useTranslation(); if (vehicleType || !config.allowVehicleTypeSelection) { - return ; + return ; } return ( diff --git a/apps/demo-app/src/pages/pages.ts b/apps/demo-app/src/pages/pages.ts index 4d359e618..40a7f3a23 100644 --- a/apps/demo-app/src/pages/pages.ts +++ b/apps/demo-app/src/pages/pages.ts @@ -2,5 +2,7 @@ export enum Page { LOG_IN = '/log-in', CREATE_INSPECTION = '/create-inspection', PHOTO_CAPTURE = '/photo-capture', + DAMAGE_DISCLOSURE = '/damage-disclosure', VEHICLE_TYPE_SELECTION = '/vehicle-type-selection', + CAPTURE_SELECTION = '/capture-selection', } diff --git a/apps/demo-app/test/pages/PhotoCapturePage.test.tsx b/apps/demo-app/test/pages/PhotoCapturePage.test.tsx index 187a62d64..82894cf9a 100644 --- a/apps/demo-app/test/pages/PhotoCapturePage.test.tsx +++ b/apps/demo-app/test/pages/PhotoCapturePage.test.tsx @@ -20,7 +20,7 @@ const appState = { enforceOrientation: 'test-enforceOrientation-test', maxUploadDurationWarning: 'test-maxUploadDurationWarning-test', allowSkipRetake: 'test-allowSkipRetake-test', - enableAddDamage: 'test-enableAddDamage-test', + addDamage: 'test-addDamage-test', enableCompliance: 'test-enableCompliance-test', enableCompliancePerSight: 'test-enableCompliancePerSight-test', complianceIssues: 'test-complianceIssues-test', @@ -53,7 +53,7 @@ describe('PhotoCapture page', () => { enforceOrientation: appState.config.enforceOrientation, maxUploadDurationWarning: appState.config.maxUploadDurationWarning, allowSkipRetake: appState.config.allowSkipRetake, - enableAddDamage: appState.config.enableAddDamage, + addDamage: appState.config.addDamage, enableCompliance: appState.config.enableCompliance, enableCompliancePerSight: appState.config.enableCompliancePerSight, complianceIssues: appState.config.complianceIssues, diff --git a/configs/test-utils/src/__mocks__/@monkvision/common.tsx b/configs/test-utils/src/__mocks__/@monkvision/common.tsx index b0732b070..6c69763c8 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/common.tsx +++ b/configs/test-utils/src/__mocks__/@monkvision/common.tsx @@ -37,6 +37,8 @@ const { mergeValidationFunctions, required, email, + getVehicleModel, + getAspectRatio, } = jest.requireActual('@monkvision/common'); export = { @@ -67,6 +69,8 @@ export = { mergeValidationFunctions, required, email, + getVehicleModel, + getAspectRatio, /* Mocks */ useMonkTheme: jest.fn(() => createTheme()), diff --git a/configs/test-utils/src/__mocks__/@monkvision/sights.ts b/configs/test-utils/src/__mocks__/@monkvision/sights.ts index f0b388214..e1d937901 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/sights.ts +++ b/configs/test-utils/src/__mocks__/@monkvision/sights.ts @@ -20,6 +20,12 @@ const vehicles = { model: 'F-150 Super Cab XL 2014', type: VehicleType.PICKUP, }, + [VehicleModel.AUDIA7]: { + id: VehicleModel.AUDIA7, + make: 'Audi', + model: 'A7', + type: VehicleType.HATCHBACK, + }, }; const labels = { diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index ac043d3da..de0d5deea 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -20,6 +20,44 @@ export const MonkJsConfig: PhotoCaptureAppConfig = { This configuration object can then be passed to components like `` or ``. +## Available Configuration Options +The following table lists the available configuration options in the `CaptureAppConfig` interface : + +| Name | Type | Description | Required | Default Value | +|------------------------------------|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|-----------------------------| +| allowManualLogin | `boolean` | Indicates if manual login and logout should be enabled or not. | ✔️ | | +| fetchFromSearchParams | `boolean` | Indicates if the app state (auth token, inspection ID etc.) should be fetched from the URL search params. | ✔️ | | +| allowVehicleTypeSelection | `boolean` | Indicates if manual vehicle type selection should be enabled if the vehicle type is not defined. | ✔️ | | +| enableSteeringWheelPosition | `boolean` | Indicates if the capture Sights should vary based on the steering wheel position (right or left). | ✔️ | | +| defaultVehicleType | `VehicleType` | Default vehicle type to use if no vehicle type has been specified. | ✔️ | | +| defaultSteeringWheelPosition | `SteeringWheelPosition` | Default steering wheel position to use if no steering wheel position has been specified. | if `enableSteeringWheelPosition` is set to `true` | | +| sights | `Record<..., string[]>` | A map associating each vehicle type supported by the app to a list of sight IDs. If `enableSteeringWheelPosition` is set to `true`, it's a map associating each steering wheel position to this map. | ✔️ | | +| allowCreateInspection | `boolean` | Indicates if automatic inspection creation should be enabled in the app. | ✔️ | | +| createInspectionOptions | `CreateInspectionOptions` | Options used when automatically creating an inspection. | if `allowCreateInspection` is set to `true` | | +| apiDomain | `string` | The API domain used to communicate with the API. | ✔️ | | +| requiredApiPermissions | `MonkApiPermission[]` | Required API permission that the user must have to use the current app. | | | +| palette | `Partial` | Custom color palette to use in the app. | | | +| enforceOrientation | `DeviceOrientation` | Use this prop to enforce a specific device orientation for the Camera screen. | | | +| maxUploadDurationWarning | `number` | Max upload duration in milliseconds before showing a bad connection warning to the user. Use `-1` to never display the warning. | | `15000` | +| useAdaptiveImageQuality | `boolean` | Boolean indicating if the image quality should be downgraded automatically in case of low connection. | | `true` | +| showCloseButton | `boolean` | Indicates if the close button should be displayed in the HUD on top of the Camera preview. | | `false` | +| startTasksOnComplete | `boolean | TaskName[]` | Value indicating if tasks should be started at the end of the inspection. See the `inspection-capture-web` package doc for more info. | | `true` | +| additionalTasks | `TaskName[]` | An optional list of additional tasks to run on every Sight of the inspection. | | | +| tasksBySight | `Record` | Record associating each sight with a list of tasks to execute for it. If not provided, the default tasks of the sight will be used. | | | +| resolution | `CameraResolution` | Indicates the resolution of the pictures taken by the Camera. | | `CameraResolution.UHD_4K` | +| allowImageUpscaling | `boolean` | Allow images to be scaled up if the device does not support the specified resolution in the `resolution` prop. | | `false` | +| format | `CompressionFormat` | The output format of the compression. | | `CompressionFormat.JPEG` | +| quality | `number` | Value indicating image quality for the compression output. | | `0.6` | +| addDamage | `AddDamage` | Options for Add Damage, If disabled, the `Add Damage` button will be hidden. | | `AddDamage.PART_SELECT` | +| allowSkipRetake | `boolean` | If compliance is enabled, this prop indicate if the user is allowed to skip the retaking process if some pictures are not compliant. | | `false` | +| enableCompliance | `boolean` | Indicates if compliance checks should be enabled or not. | | `true` | +| enableCompliancePerSight | `string[]` | Array of Sight IDs that indicates for which sight IDs the compliance should be enabled. | | | +| complianceIssues | `ComplianceIssue[]` | If compliance checks are enabled, this property can be used to select a list of compliance issues to check. | | `DEFAULT_COMPLIANCE_ISSUES` | +| complianceIssuesPerSight | `Record` | A map associating Sight IDs to a list of compliance issues to check. | | | +| useLiveCompliance | `boolean` | Indicates if live compliance should be enabled or not. | | `false` | +| customComplianceThresholds | `CustomComplianceThresholds` | Custom thresholds that can be used to modify the strictness of the compliance for certain compliance issues. | | | +| customComplianceThresholdsPerSight | `Record` | A map associating Sight IDs to custom compliance thresholds. | | | + ## Live Configs MonkJs now also offers a way to configure Live Configurations for your web applications. This allows MonkJs apps to fetch their configurations (`PhotoCaptureAppConfig` or `VideoCaptureAppConfig`) from a GCP Bucket on startup. By doing diff --git a/documentation/docs/photo-capture-workflow.md b/documentation/docs/photo-capture-workflow.md index 5490e6453..682be0f81 100644 --- a/documentation/docs/photo-capture-workflow.md +++ b/documentation/docs/photo-capture-workflow.md @@ -58,7 +58,7 @@ increase the detection rate. This feature is called `Add Damage`, and there two take a close-up picture of the damage. For now, only the 2-shot workflow is implemented in the PhotoCapture workflow. This feature is enabled by default in the -`PhotoCapture` component. To disable it, pass the `enableAddDamage` prop to `false`. +`PhotoCapture` component. To disable it, pass the `addDamage` prop to `AddDamage.DISABLED`. ## Using Compliance The compliance is a feature that allows our AI models to analyze the quality of the pictures taken by the user, and if diff --git a/documentation/src/utils/schemas.ts b/documentation/src/utils/schemas.ts new file mode 100644 index 000000000..356fae305 --- /dev/null +++ b/documentation/src/utils/schemas.ts @@ -0,0 +1,328 @@ +import { z, CustomErrorParams } from 'zod'; +import { + AddDamage, + CameraResolution, + ComplianceIssue, + CompressionFormat, + CurrencyCode, + DeviceOrientation, + MileageUnit, + MonkApiPermission, + PhotoCaptureTutorialOption, + SteeringWheelPosition, + TaskName, + VehicleType, +} from '@monkvision/types'; +import { sights } from '@monkvision/sights'; +import { flatten } from '@monkvision/common'; + +function isValidSightId(sightId: string): boolean { + return !!sights[sightId]; +} + +function validateSightIds(value?: string[] | Record): boolean { + if (!value) { + return true; + } + const sightIds = Array.isArray(value) ? value : Object.keys(value); + return sightIds.every(isValidSightId); +} + +function getInvalidSightIdsMessage(value?: string[] | Record): CustomErrorParams { + if (!value) { + return {}; + } + const sightIds = Array.isArray(value) ? value : Object.keys(value); + const invalidIds = sightIds.filter((sightId) => !isValidSightId(sightId)).join(', '); + const plural = invalidIds.length > 1 ? 's' : ''; + return { message: `Invalid sight ID${plural} : ${invalidIds}` }; +} + +function getAllSightsByVehicleType( + vehicleSights?: Partial>, +): string[] | undefined { + return vehicleSights ? flatten(Object.values(vehicleSights)) : undefined; +} + +export const CompressionOptionsSchema = z.object({ + format: z.nativeEnum(CompressionFormat), + quality: z.number().gte(0).lte(1), +}); + +export const CameraConfigSchema = z + .object({ + resolution: z.nativeEnum(CameraResolution).optional(), + allowImageUpscaling: z.boolean().optional(), + }) + .and(CompressionOptionsSchema.partial()); + +export const CustomComplianceThresholdsSchema = z + .object({ + blurriness: z.number().gte(0).lte(1).optional(), + overexposure: z.number().gte(0).lte(1).optional(), + underexposure: z.number().gte(0).lte(1).optional(), + lensFlare: z.number().gte(0).lte(1).optional(), + wetness: z.number().gte(0).lte(1).optional(), + snowness: z.number().gte(0).lte(1).optional(), + dirtiness: z.number().gte(0).lte(1).optional(), + reflections: z.number().gte(0).lte(1).optional(), + zoom: z + .object({ + min: z.number().gte(0).lte(1), + max: z.number().gte(0).lte(1), + }) + .optional(), + }) + .refine((thresholds) => !thresholds.zoom || thresholds.zoom.min < thresholds.zoom.max, { + message: 'Min zoom threshold must be smaller than max zoom threshold', + }); + +export const ComplianceOptionsSchema = z.object({ + enableCompliance: z.boolean().optional(), + enableCompliancePerSight: z + .array(z.string()) + .optional() + .refine(validateSightIds, getInvalidSightIdsMessage), + complianceIssues: z.array(z.nativeEnum(ComplianceIssue)).optional(), + complianceIssuesPerSight: z + .record(z.string(), z.array(z.nativeEnum(ComplianceIssue))) + .optional() + .refine(validateSightIds, getInvalidSightIdsMessage), + useLiveCompliance: z.boolean().optional(), + customComplianceThresholds: CustomComplianceThresholdsSchema.optional(), + customComplianceThresholdsPerSight: z + .record(z.string(), CustomComplianceThresholdsSchema) + .optional() + .refine(validateSightIds, getInvalidSightIdsMessage), +}); + +export const SightGuidelineSchema = z.object({ + sightIds: z.array(z.string()), + en: z.string(), + fr: z.string(), + de: z.string(), + nl: z.string(), +}); + +export const AccentColorVariantsSchema = z.object({ + xdark: z.string(), + dark: z.string(), + base: z.string(), + light: z.string(), + xlight: z.string(), +}); + +export const TextColorVariantsSchema = z.object({ + primary: z.string(), + secondary: z.string(), + disabled: z.string(), + white: z.string(), + black: z.string(), + link: z.string(), + linkInverted: z.string(), +}); + +export const BackgroundColorVariantsSchema = z.object({ + dark: z.string(), + base: z.string(), + light: z.string(), +}); + +export const SurfaceColorVariantsSchema = z.object({ + dark: z.string(), + light: z.string(), +}); + +export const OutlineColorVariantsSchema = z.object({ + base: z.string(), +}); + +export const MonkPaletteSchema = z.object({ + primary: AccentColorVariantsSchema, + secondary: AccentColorVariantsSchema, + alert: AccentColorVariantsSchema, + caution: AccentColorVariantsSchema, + success: AccentColorVariantsSchema, + information: AccentColorVariantsSchema, + text: TextColorVariantsSchema, + background: BackgroundColorVariantsSchema, + surface: SurfaceColorVariantsSchema, + outline: OutlineColorVariantsSchema, +}); + +export const SightsByVehicleTypeSchema = z + .record(z.nativeEnum(VehicleType), z.array(z.string())) + .refine( + (vehicleSights) => validateSightIds(getAllSightsByVehicleType(vehicleSights)), + (vehicleSights) => getInvalidSightIdsMessage(getAllSightsByVehicleType(vehicleSights)), + ); + +export const SteeringWheelDiscriminatedUnionSchema = z.discriminatedUnion( + 'enableSteeringWheelPosition', + [ + z.object({ + enableSteeringWheelPosition: z.literal(false), + sights: SightsByVehicleTypeSchema, + }), + z.object({ + enableSteeringWheelPosition: z.literal(true), + defaultSteeringWheelPosition: z.nativeEnum(SteeringWheelPosition), + sights: z.record(z.nativeEnum(SteeringWheelPosition), SightsByVehicleTypeSchema), + }), + ], +); + +export const TaskCallbackOptionsSchema = z.object({ + url: z.string(), + headers: z.record(z.string(), z.string()), + params: z.record(z.string(), z.unknown()).optional(), + event: z.string().optional(), +}); + +export const CreateDamageDetectionTaskOptionsSchema = z.object({ + name: z.literal(TaskName.DAMAGE_DETECTION), + damageScoreThreshold: z.number().gte(0).lte(1).optional(), + generateDamageVisualOutput: z.boolean().optional(), + generateSubimageDamages: z.boolean().optional(), + generateSubimageParts: z.boolean().optional(), +}); + +export const CreateHinlTaskOptionsSchema = z.object({ + name: z.literal(TaskName.HUMAN_IN_THE_LOOP), + callbacks: z.array(TaskCallbackOptionsSchema).optional(), +}); + +export const CreatePricingTaskOptionsSchema = z.object({ + name: z.literal(TaskName.PRICING), + outputFormat: z.string().optional(), + config: z.string().optional(), + methodology: z.string().optional(), +}); + +export const InspectionCreateTaskSchema = z + .nativeEnum(TaskName) + .or(CreateDamageDetectionTaskOptionsSchema) + .or(CreateHinlTaskOptionsSchema) + .or(CreatePricingTaskOptionsSchema); + +export const AdditionalDataSchema = z.record(z.string(), z.unknown()); + +export const InspectionCreateVehicleSchema = z.object({ + brand: z.string().optional(), + model: z.string().optional(), + plate: z.string().optional(), + type: z.string().optional(), + mileageUnit: z.nativeEnum(MileageUnit).optional(), + mileageValue: z.number().optional(), + marketValueUnit: z.nativeEnum(CurrencyCode).optional(), + marketValue: z.number().optional(), + vin: z.string().optional(), + color: z.string().optional(), + exteriorCleanliness: z.string().optional(), + interiorCleanliness: z.string().optional(), + dateOfCirculation: z.string().optional(), + duplicateKeys: z.boolean().optional(), + expertiseRequested: z.boolean().optional(), + carRegistration: z.boolean().optional(), + vehicleQuotation: z.number().optional(), + tradeInOffer: z.number().optional(), + ownerInfo: z.record(z.string().optional(), z.unknown()).optional(), + additionalData: AdditionalDataSchema.optional(), +}); + +export const CreateInspectionOptionsSchema = z.object({ + tasks: z.array(InspectionCreateTaskSchema), + vehicle: InspectionCreateVehicleSchema.optional(), + useDynamicCrops: z.boolean().optional(), + enablePricingV1: z.boolean().optional(), + additionalData: AdditionalDataSchema.optional(), +}); + +export const CreateInspectionDiscriminatedUnionSchema = z.discriminatedUnion( + 'allowCreateInspection', + [ + z.object({ + allowCreateInspection: z.literal(false), + }), + z.object({ + allowCreateInspection: z.literal(true), + createInspectionOptions: CreateInspectionOptionsSchema, + }), + ], +); + +const domainsByEnv = { + staging: { + api: 'api.staging.monk.ai/v1', + thumbnail: 'europe-west1-monk-staging-321715.cloudfunctions.net/image_resize', + }, + preview: { + api: 'api.preview.monk.ai/v1', + thumbnail: 'europe-west1-monk-preview-321715.cloudfunctions.net/image_resize', + }, + production: { + api: 'api.monk.ai/v1', + thumbnail: 'europe-west1-monk-prod.cloudfunctions.net/image_resize', + }, +}; + +const apiDomains = Object.values(domainsByEnv).map((env) => env.api) as [string, ...string[]]; +const thumbnailDomains = Object.values(domainsByEnv).map((env) => env.thumbnail) as [ + string, + ...string[], +]; + +export const DomainsSchema = z + .object({ + apiDomain: z.enum(apiDomains), + thumbnailDomain: z.enum(thumbnailDomains), + }) + .refine( + (data) => { + const apiEnv = Object.values(domainsByEnv).find((env) => env.api === data.apiDomain); + const thumbnailEnv = Object.values(domainsByEnv).find( + (env) => env.thumbnail === data.thumbnailDomain, + ); + return !!apiEnv && apiEnv === thumbnailEnv; + }, + (data) => ({ + message: `The selected thumbnailDomain must correspond to the selected apiDomain. Please use the corresponding thumbnailDomain: ${ + thumbnailDomains[apiDomains.indexOf(data.apiDomain)] + }`, + path: ['thumbnailDomain'], + }), + ); + +export const LiveConfigSchema = z + .object({ + id: z.string(), + description: z.string(), + additionalTasks: z.array(z.nativeEnum(TaskName)).optional(), + tasksBySight: z.record(z.string(), z.array(z.nativeEnum(TaskName))).optional(), + startTasksOnComplete: z + .boolean() + .or(z.array(z.nativeEnum(TaskName))) + .optional(), + showCloseButton: z.boolean().optional(), + enforceOrientation: z.nativeEnum(DeviceOrientation).optional(), + maxUploadDurationWarning: z.number().positive().or(z.literal(-1)).optional(), + useAdaptiveImageQuality: z.boolean().optional(), + allowSkipRetake: z.boolean().optional(), + addDamage: z.nativeEnum(AddDamage).optional(), + enableSightGuidelines: z.boolean().optional(), + sightGuidelines: z.array(SightGuidelineSchema).optional(), + enableTutorial: z.nativeEnum(PhotoCaptureTutorialOption).optional(), + allowSkipTutorial: z.boolean().optional(), + enableSightTutorial: z.boolean().optional(), + defaultVehicleType: z.nativeEnum(VehicleType), + allowManualLogin: z.boolean(), + allowVehicleTypeSelection: z.boolean(), + fetchFromSearchParams: z.boolean(), + requiredApiPermissions: z.array(z.nativeEnum(MonkApiPermission)).optional(), + palette: MonkPaletteSchema.partial().optional(), + }) + .and(DomainsSchema) + .and(SteeringWheelDiscriminatedUnionSchema) + .and(CreateInspectionDiscriminatedUnionSchema) + .and(CameraConfigSchema) + .and(ComplianceOptionsSchema); diff --git a/packages/common-ui-web/README.md b/packages/common-ui-web/README.md index 8fae5a264..25fd92885 100644 --- a/packages/common-ui-web/README.md +++ b/packages/common-ui-web/README.md @@ -134,6 +134,37 @@ function App() { --- +## CaptureSelection +### Description +A single page component that allows the user to select between "Add Damage" or "Photo Capture" workflow. + +### Example + +```tsx +import { CaptureSelection } from "@monkvision/common-ui-web"; +import { useNavigate } from "react-router-dom"; + +function App() { + const { navigate } = useNavigate(); + + return ( + navigate('/add-damage-page')} + onCapture={() => navigate('/photo-capture-page')} + /> + ); +} +``` + +### Props +| Prop | Type | Description | Required | Default Value | +|-------------|------------|----------------------------------------------------------------|----------|---------------| +| lang | string | The language used by the component. | | `'en'` | +| onAddDamage | () => void | Callback called when the user clicks on "Add Damage" button. | | | +| onCapture | () => void | Callback called when the user clicks on "Take Picture" button. | | | + +--- + ## Checkbox ### Description Custom component implementing a simple checkbox. diff --git a/packages/common-ui-web/src/components/CaptureSelection/CaptureSelection.styles.ts b/packages/common-ui-web/src/components/CaptureSelection/CaptureSelection.styles.ts new file mode 100644 index 000000000..b88b8d102 --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/CaptureSelection.styles.ts @@ -0,0 +1,62 @@ +import { Styles } from '@monkvision/types'; + +export const SMALL_WIDTH_BREAKPOINT = 700; + +export const styles: Styles = { + container: { + height: '100%', + width: '100%', + position: 'fixed', + inset: 0, + display: 'flex', + justifyContent: 'space-evenly', + alignItems: 'center', + zIndex: 10, + gap: '15px', + flexDirection: 'row', + }, + containerSmall: { + __media: { maxWidth: SMALL_WIDTH_BREAKPOINT }, + justifyContent: 'center', + flexDirection: 'column', + }, + elementsContainer: { + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + }, + contentContainer: { + width: '40%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '10px', + flexDirection: 'column', + paddingBottom: '20px', + }, + contentContainerSmall: { + __media: { maxWidth: SMALL_WIDTH_BREAKPOINT }, + width: '80%', + }, + textcontainer: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '10px', + flexDirection: 'column', + }, + title: { + fontSize: '22px', + }, + description: { + fontSize: '14px', + textAlign: 'center', + paddingBottom: '10px', + }, + buttonsContainer: { + display: 'flex', + width: '100%', + justifyContent: 'space-between', + gap: '3px', + }, +}; diff --git a/packages/common-ui-web/src/components/CaptureSelection/CaptureSelection.tsx b/packages/common-ui-web/src/components/CaptureSelection/CaptureSelection.tsx new file mode 100644 index 000000000..5e015134c --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/CaptureSelection.tsx @@ -0,0 +1,73 @@ +import { useTranslation } from 'react-i18next'; +import { i18nWrap, useI18nSync, useMonkTheme } from '@monkvision/common'; +import { styles } from './CaptureSelection.styles'; +import { Button } from '../Button'; +import { i18nCreateInspection } from './i18n'; +import { useCaptureSelectionStyles } from './hooks'; + +/** + * Props of the CaptureSelection component. + */ +export interface CaptureSelectionProps { + /** + * The language used by the component. + * + * @default en + */ + lang?: string; + /** + * Callback called when the user clicks on "Add Damage" button. + */ + onAddDamage?: () => void; + /** + * Callback called when the user clicks on "Take Picture" button. + */ + onCapture?: () => void; +} + +/** + * A single page component that allows the user to select between "Add Damage" or "Photo Capture" workflow. + */ +export const CaptureSelection = i18nWrap(function CaptureSelection({ + lang, + onAddDamage = () => {}, + onCapture = () => {}, +}: CaptureSelectionProps) { + useI18nSync(lang); + const { t } = useTranslation(); + const { palette } = useMonkTheme(); + const style = useCaptureSelectionStyles(); + + return ( +
+
+
+ {t('addDamage.title')} + {t('addDamage.description')} +
+ +
+
+ {t('capture.title')} + {t('capture.description')} + +
+
+ ); +}, +i18nCreateInspection); diff --git a/packages/common-ui-web/src/components/CaptureSelection/hooks.ts b/packages/common-ui-web/src/components/CaptureSelection/hooks.ts new file mode 100644 index 000000000..d352cc2e7 --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/hooks.ts @@ -0,0 +1,19 @@ +import { useMonkTheme, useResponsiveStyle } from '@monkvision/common'; +import { styles } from './CaptureSelection.styles'; + +export function useCaptureSelectionStyles() { + const { responsive } = useResponsiveStyle(); + const { palette } = useMonkTheme(); + + return { + container: { + ...styles['container'], + ...responsive(styles['containerSmall']), + }, + contentContainer: { + ...styles['contentContainer'], + ...responsive(styles['contentContainerSmall']), + }, + description: { color: palette.secondary.xlight, ...styles['description'] }, + }; +} diff --git a/packages/common-ui-web/src/components/CaptureSelection/i18n.ts b/packages/common-ui-web/src/components/CaptureSelection/i18n.ts new file mode 100644 index 000000000..b7c0890e4 --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/i18n.ts @@ -0,0 +1,20 @@ +import { i18nCreateSDKInstance } from '@monkvision/common'; +import en from './translations/en.json'; +import fr from './translations/fr.json'; +import de from './translations/de.json'; +import nl from './translations/nl.json'; + +/** + * i18n instance of the CaptureSelection component. You can use this instance to automatically sync your application + * current language with the one used by the components of the package. + */ +const i18nCreateInspection = i18nCreateSDKInstance({ + resources: { + en: { translation: en }, + fr: { translation: fr }, + de: { translation: de }, + nl: { translation: nl }, + }, +}); + +export { i18nCreateInspection }; diff --git a/packages/common-ui-web/src/components/CaptureSelection/index.ts b/packages/common-ui-web/src/components/CaptureSelection/index.ts new file mode 100644 index 000000000..3cea91b95 --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/index.ts @@ -0,0 +1 @@ +export * from './CaptureSelection'; diff --git a/packages/common-ui-web/src/components/CaptureSelection/translations/de.json b/packages/common-ui-web/src/components/CaptureSelection/translations/de.json new file mode 100644 index 000000000..31ae3f0c7 --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/translations/de.json @@ -0,0 +1,12 @@ +{ + "addDamage": { + "title": "Schaden melden", + "description": "Fügen Sie Fotos von Dellen, Kratzern oder anderen Schäden am Fahrzeug hinzu.", + "button": "Schadensfotos hinzufügen" + }, + "capture": { + "title": "Zustand erfassen", + "description": "Keine Dellen oder Kratzer? Gehen Sie um das Fahrzeug herum und machen Sie Fotos.", + "button": "Fotos machen" + } +} diff --git a/packages/common-ui-web/src/components/CaptureSelection/translations/en.json b/packages/common-ui-web/src/components/CaptureSelection/translations/en.json new file mode 100644 index 000000000..7a6eaadad --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/translations/en.json @@ -0,0 +1,12 @@ +{ + "addDamage": { + "title": "Report Damage", + "description": "Add photos of dents, scratches or other damages present on the vehicle.", + "button": "Add Damage Photos" + }, + "capture": { + "title": "Capture Condition", + "description": "No dents or scratches? Walk around the vehicle and take photos.", + "button": "Take Photos" + } +} diff --git a/packages/common-ui-web/src/components/CaptureSelection/translations/fr.json b/packages/common-ui-web/src/components/CaptureSelection/translations/fr.json new file mode 100644 index 000000000..be5d8615c --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/translations/fr.json @@ -0,0 +1,12 @@ +{ + "addDamage": { + "title": "Signaler un dégât", + "description": "Ajoutez des photos d'enfoncements, rayures ou autres dégâts présents sur le véhicule.", + "button": "Ajouter des photos de dégâts" + }, + "capture": { + "title": "Capturer l'état", + "description": "Pas d'enfoncements ni de rayures ? Faites le tour du véhicule et prenez des photos.", + "button": "Prendre des photos" + } +} diff --git a/packages/common-ui-web/src/components/CaptureSelection/translations/nl.json b/packages/common-ui-web/src/components/CaptureSelection/translations/nl.json new file mode 100644 index 000000000..9b4d2786a --- /dev/null +++ b/packages/common-ui-web/src/components/CaptureSelection/translations/nl.json @@ -0,0 +1,12 @@ +{ + "addDamage": { + "title": "Schade melden", + "description": "Voeg foto's toe van deuken, krassen of andere schade aan het voertuig.", + "button": "Schadefoto's toevoegen" + }, + "capture": { + "title": "Conditie vastleggen", + "description": "Geen deuken of krassen? Loop rond het voertuig en maak foto's.", + "button": "Foto's maken" + } +} diff --git a/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts b/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts index 576b3c14c..9cf3eddc6 100644 --- a/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts +++ b/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { ImageStatus, Sight } from '@monkvision/types'; +import { AddDamage, ImageStatus, ImageType, Sight } from '@monkvision/types'; import { getInspectionImages, MonkState, useMonkState } from '@monkvision/common'; import { useInspectionPoll } from '@monkvision/network'; import { InspectionGalleryItem, InspectionGalleryProps } from '../types'; @@ -57,9 +57,10 @@ function getItems( captureMode: boolean, entities: MonkState, inspectionSights?: Sight[], - enableAddDamage?: boolean, + addDamage?: AddDamage, + filterByImageType?: ImageType, ): InspectionGalleryItem[] { - const images = getInspectionImages(inspectionId, entities.images, captureMode); + const images = getInspectionImages(inspectionId, entities.images, filterByImageType, captureMode); const items: InspectionGalleryItem[] = images.map((image) => ({ isTaken: true, isAddDamage: false, @@ -73,7 +74,7 @@ function getItems( items.push({ isTaken: false, isAddDamage: false, sightId: sight.id }); } }); - if (captureMode && enableAddDamage !== false) { + if (captureMode && addDamage !== AddDamage.DISABLED) { items.push({ isAddDamage: true }); } return items.sort((a, b) => compareGalleryItems(a, b, captureMode, inspectionSights)); @@ -99,9 +100,17 @@ export function useInspectionGalleryItems(props: InspectionGalleryProps): Inspec props.captureMode, state, inspectionSights, - props.enableAddDamage, + props.addDamage, + props.filterByImageType, ), - [props.inspectionId, props.captureMode, state, inspectionSights, props.enableAddDamage], + [ + props.inspectionId, + props.captureMode, + state, + inspectionSights, + props.addDamage, + props.filterByImageType, + ], ); const shouldFetch = useMemo(() => shouldContinueToFetch(items), items); diff --git a/packages/common-ui-web/src/components/InspectionGallery/types.ts b/packages/common-ui-web/src/components/InspectionGallery/types.ts index 01fbe9410..b68cdc960 100644 --- a/packages/common-ui-web/src/components/InspectionGallery/types.ts +++ b/packages/common-ui-web/src/components/InspectionGallery/types.ts @@ -1,4 +1,4 @@ -import { ComplianceOptions, Image, Sight } from '@monkvision/types'; +import { AddDamage, ComplianceOptions, Image, ImageType, Sight } from '@monkvision/types'; import { MonkApiConfig } from '@monkvision/network'; /** @@ -100,12 +100,17 @@ export type InspectionGalleryProps = { */ onValidate?: () => void; /** - * Boolean indicating if `Add Damage` feature should be enabled or not. If disabled, the `Add Custom Damage` button - * will be hidden. + * Options for Add Damage. If disabled, the `Add Custom Damage` button will be hidden. * - * @default true + * @default AddDamage.PART_SELECT. */ - enableAddDamage?: boolean; + addDamage?: AddDamage; + /** + * The specific image type to filter by. If not provided, no type-based filtering is applied. + * + * @default undefined + */ + filterByImageType?: ImageType; /** * Custom label for validate button. */ diff --git a/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.style.ts b/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.style.ts index b15a0fd9c..7932bb937 100644 --- a/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.style.ts +++ b/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.style.ts @@ -8,4 +8,8 @@ export const styles: Styles = { pointerEvents: 'fill', cursor: 'pointer', }, + svg: { + width: '100%', + height: '100%', + }, }; diff --git a/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx b/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx index 74a92ab6f..b040d210f 100644 --- a/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx +++ b/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx @@ -1,106 +1,23 @@ -import { useMonkTheme } from '@monkvision/common'; -import { - PartSelectionOrientation, - VehicleModel, - VehiclePart, - VehicleType, -} from '@monkvision/types'; -import { SVGProps } from 'react'; -import { partSelectionWireframes, vehicles } from '@monkvision/sights'; -import { DynamicSVG, DynamicSVGCustomizationFunctions } from '../DynamicSVG'; +import { PartSelectionOrientation } from '@monkvision/types'; +import { DynamicSVG } from '../DynamicSVG'; +import { useVehicleDynamicWireframe, VehicleDynamicWireframeProps } from './hooks'; import { styles } from './VehicleDynamicWireframe.style'; -/** - * Props accepted by the VehicleDynamicWireframe component. - */ -export interface VehicleDynamicWireframeProps { - /** - * Vehicle type to display the wireframe for. - */ - vehicleType: VehicleType; - /** - * The orientation of the wireframe. - * - * @default PartSelectionOrientation.FRONT_LEFT - */ - orientation?: PartSelectionOrientation; - /** - * Callback when the user clicks a part. - */ - onClickPart?: (parts: VehiclePart) => void; - /** - * Callback used to customize the display style of each vehicle part on the wireframe. - * See `DynamicSVGCustomizationFunctions` for more details. - * - * @see DynamicSVGCustomizationFunctions - */ - getPartAttributes?: (part: VehiclePart) => SVGProps; -} - -function isCarPartElement(element: SVGElement) { - return element.id !== '' && element.classList.contains('car-part'); -} - -function createGetAttributesCallback( - onClickPart: NonNullable, - getPartAttributes: NonNullable, -): NonNullable { - return (element, groups) => { - const groupElement: SVGGElement | undefined = groups[0]; - let part: VehiclePart; - if (groupElement && isCarPartElement(groupElement)) { - part = groupElement.id as VehiclePart; - } else if (isCarPartElement(element)) { - part = element.id as VehiclePart; - } else { - return { style: styles['notCarPart'] }; - } - const attributes = getPartAttributes(part); - if (element.tagName === 'g') { - delete attributes.style; - } - if (element.classList.contains('selectable') && element.id) { - attributes.onClick = () => onClickPart(part); - attributes.style = { ...attributes.style, ...styles['selectable'] }; - } - return attributes; - }; -} - -function getVehicleModel(vehicleType: VehicleType): VehicleModel { - const detail = Object.entries(vehicles) - .filter(([type]) => type !== VehicleModel.AUDIA7) - .find(([, details]) => details.type === vehicleType)?.[1]; - if (detail === undefined) { - throw new Error(`No vehicle model found for vehicle type ${vehicleType}`); - } - return detail.id; -} - /** * Component that displays a dynamic wireframe of a vehicle, allowing the user to select parts of the vehicle. */ export function VehicleDynamicWireframe({ vehicleType, - orientation = PartSelectionOrientation.FRONT_LEFT, onClickPart = () => {}, + orientation = PartSelectionOrientation.FRONT_LEFT, getPartAttributes = () => ({}), }: VehicleDynamicWireframeProps) { - const wireframes = partSelectionWireframes[getVehicleModel(vehicleType)]; - if (wireframes === undefined) { - throw new Error(`No wireframe found for vehicle type ${vehicleType}`); - } - const overlay = wireframes[orientation]; - const { utils } = useMonkTheme(); + const { overlay, getAttributes } = useVehicleDynamicWireframe({ + vehicleType, + orientation, + onClickPart, + getPartAttributes, + }); - return ( - - ); + return ; } diff --git a/packages/common-ui-web/src/components/VehicleDynamicWireframe/hooks.ts b/packages/common-ui-web/src/components/VehicleDynamicWireframe/hooks.ts new file mode 100644 index 000000000..52c94e7a9 --- /dev/null +++ b/packages/common-ui-web/src/components/VehicleDynamicWireframe/hooks.ts @@ -0,0 +1,82 @@ +import { partSelectionWireframes } from '@monkvision/sights'; +import { getVehicleModel } from '@monkvision/common'; +import { VehicleType, VehiclePart, PartSelectionOrientation } from '@monkvision/types'; +import { SVGProps, useCallback, useMemo } from 'react'; +import { styles } from './VehicleDynamicWireframe.style'; + +function isCarPartElement(element: SVGElement): boolean { + return element.id !== '' && element.classList.contains('car-part'); +} + +function getWireframes(vehicleType: VehicleType, orientation: PartSelectionOrientation): string { + const wireframes = partSelectionWireframes[getVehicleModel(vehicleType)]; + if (wireframes === undefined) { + throw new Error(`No wireframe found for vehicle type ${vehicleType}`); + } + return wireframes[orientation]; +} + +/** + * Props accepted by the VehicleDynamicWireframe component. + */ +export interface VehicleDynamicWireframeProps { + /** + * The type of vehicle for which the wireframe will be displayed. + */ + vehicleType: VehicleType; + /** + * The orientation of the wireframe. + * + * @default PartSelectionOrientation.FRONT_LEFT + */ + orientation?: PartSelectionOrientation; + /** + * Callback when the user clicks on a vehicle part. + */ + onClickPart?: (parts: VehiclePart) => void; + /** + * Customizes the display attributes (e.g., styles, colors) of vehicle parts. + * See `DynamicSVGCustomizationFunctions` for more details. + * + * @see DynamicSVGCustomizationFunctions + */ + getPartAttributes?: (part: VehiclePart) => SVGProps; +} + +export function useVehicleDynamicWireframe({ + vehicleType, + orientation = PartSelectionOrientation.FRONT_LEFT, + onClickPart = () => {}, + getPartAttributes = () => ({}), +}: VehicleDynamicWireframeProps) { + const overlay = useMemo( + () => getWireframes(vehicleType, orientation), + [vehicleType, orientation], + ); + + const getAttributes = useCallback( + (element, groups) => { + const groupElement: SVGGElement | undefined = groups[0]; + let part: VehiclePart; + if (groupElement && isCarPartElement(groupElement)) { + part = groupElement.id as VehiclePart; + } else if (isCarPartElement(element)) { + part = element.id as VehiclePart; + } else { + return { style: styles['notCarPart'] }; + } + const attributes = getPartAttributes(part); + if (element.tagName === 'g') { + delete attributes.style; + } + if (element.classList.contains('selectable') && element.id) { + attributes.onClick = () => onClickPart(part); + attributes.style = { ...attributes.style, ...styles['selectable'] }; + } + return attributes; + }, + [onClickPart, getPartAttributes], + ); + + return { overlay, getAttributes }; +} diff --git a/packages/common-ui-web/src/components/VehicleDynamicWireframe/index.ts b/packages/common-ui-web/src/components/VehicleDynamicWireframe/index.ts index d3120c924..4e01db079 100644 --- a/packages/common-ui-web/src/components/VehicleDynamicWireframe/index.ts +++ b/packages/common-ui-web/src/components/VehicleDynamicWireframe/index.ts @@ -1,4 +1,2 @@ -export { - VehicleDynamicWireframe, - type VehicleDynamicWireframeProps, -} from './VehicleDynamicWireframe'; +export { VehicleDynamicWireframe } from './VehicleDynamicWireframe'; +export { type VehicleDynamicWireframeProps } from './hooks'; diff --git a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts index e00d5c53f..6e7d22472 100644 --- a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts +++ b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts @@ -1,12 +1,31 @@ import { Styles } from '@monkvision/types'; +export const ICON_SIZE = 40; + export const styles: Styles = { wrapper: { - position: 'absolute', display: 'flex', - justifyContent: 'center', alignItems: 'center', - inset: '0 0 0 0', - gap: '30px', + justifyContent: 'space-between', + height: '100%', + width: '100%', + }, + wireframeContainer: { + height: '90%', + width: '100%', + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + }, + leftArrowContainer: { + display: 'flex', + }, + leftArrow: { + zIndex: 1, + }, + spacer: { width: `${ICON_SIZE}px` }, + rightArrow: { + position: 'absolute', + right: 0, }, }; diff --git a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx index 50dadec22..ba4a51694 100644 --- a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx +++ b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx @@ -1,9 +1,15 @@ -import { PartSelectionOrientation, VehiclePart, VehicleType } from '@monkvision/types'; +import { + ColorProp, + PartSelectionOrientation, + Styles, + VehiclePart, + VehicleType, +} from '@monkvision/types'; import { useState } from 'react'; import { useMonkTheme } from '@monkvision/common'; import { Icon } from '../../icons'; import { VehicleDynamicWireframe, VehicleDynamicWireframeProps } from '../VehicleDynamicWireframe'; -import { styles } from './VehiclePartSelection.style'; +import { ICON_SIZE, styles } from './VehiclePartSelection.style'; /** * Props accepted by the VehiclePartSelection component @@ -23,9 +29,27 @@ export interface VehiclePartSelectionProps { * Callback called when the selected parts are updated (the user selects or unselects a new part). */ onPartsSelected?: (parts: VehiclePart[]) => void; + /** + * The name or the hexcode of the color to apply to the part selected. + * + * @default 'primary-base' + */ + primaryColor?: ColorProp; + /** + * The name or the hexcode of the color to apply to the vehicle wireframe. + * + * @default 'text-primary' + */ + secondaryColor?: ColorProp; + /** + * The maximum number of parts that can be selected. + * + * @default Infinity + */ + maxSelectableParts?: number; } -const ORIENTATIONS = [ +const ORIENTATIONS_ORDER = [ PartSelectionOrientation.FRONT_LEFT, PartSelectionOrientation.REAR_LEFT, PartSelectionOrientation.REAR_RIGHT, @@ -37,51 +61,81 @@ const ORIENTATIONS = [ */ export function VehiclePartSelection({ vehicleType, - orientation: initialOrientation, + orientation: initialOrientation = PartSelectionOrientation.FRONT_LEFT, onPartsSelected = () => {}, + primaryColor = 'primary-base', + secondaryColor = 'text-primary', + maxSelectableParts = Infinity, }: VehiclePartSelectionProps) { - const [orientation, setOrientation] = useState(initialOrientation ?? ORIENTATIONS[0]); - const [selectedParts, setSelectedParts] = useState>([]); - const { palette } = useMonkTheme(); + const [orientation, setOrientation] = useState(initialOrientation); + const [selectedParts, setSelectedParts] = useState([]); + const { utils } = useMonkTheme(); const rotateRight = () => { - const currentIndex = ORIENTATIONS.indexOf(orientation); - const nextIndex = (currentIndex + 1) % ORIENTATIONS.length; - setOrientation(ORIENTATIONS[nextIndex]); + const currentIndex = ORIENTATIONS_ORDER.indexOf(orientation); + const nextIndex = (currentIndex + 1) % ORIENTATIONS_ORDER.length; + setOrientation(ORIENTATIONS_ORDER[nextIndex]); }; const rotateLeft = () => { - const currentIndex = ORIENTATIONS.indexOf(orientation); - const nextIndex = (currentIndex - 1 + ORIENTATIONS.length) % ORIENTATIONS.length; - setOrientation(ORIENTATIONS[nextIndex]); + const currentIndex = ORIENTATIONS_ORDER.indexOf(orientation); + const nextIndex = (currentIndex - 1 + ORIENTATIONS_ORDER.length) % ORIENTATIONS_ORDER.length; + setOrientation(ORIENTATIONS_ORDER[nextIndex]); }; + const togglePart = (part: VehiclePart) => { - const newSelectedParts = selectedParts.includes(part) - ? selectedParts.filter((p) => p !== part) - : [...selectedParts, part]; - setSelectedParts(newSelectedParts); - onPartsSelected(newSelectedParts); + const isSelected = selectedParts.includes(part); + if (isSelected) { + const newSelectedParts = selectedParts.filter((p) => p !== part); + setSelectedParts(newSelectedParts); + onPartsSelected(newSelectedParts); + } else { + let newSelectedParts = [...selectedParts, part]; + if (newSelectedParts.length > maxSelectableParts) { + newSelectedParts = [...newSelectedParts.slice(1)]; + } + setSelectedParts(newSelectedParts); + onPartsSelected(newSelectedParts); + } }; + const getPartAttributes: VehicleDynamicWireframeProps['getPartAttributes'] = ( part: VehiclePart, - ) => ({ - style: { - // TODO: need to finalize the color for the selected parts. - fill: selectedParts.includes(part) ? palette.primary.xlight : undefined, - stroke: palette.primary.light, - display: 'block', - }, - }); + ): Styles => { + return { + style: { + fill: selectedParts.includes(part) ? utils.getColor(primaryColor) : undefined, + stroke: utils.getColor(secondaryColor), + }, + }; + }; return (
- - + + +
+
+ +
+ - ); } diff --git a/packages/common-ui-web/src/components/index.ts b/packages/common-ui-web/src/components/index.ts index 59784da71..d525af00f 100644 --- a/packages/common-ui-web/src/components/index.ts +++ b/packages/common-ui-web/src/components/index.ts @@ -20,3 +20,4 @@ export * from './VehiclePartSelection'; export * from './VehicleTypeAsset'; export * from './VehicleTypeSelection'; export * from './VehicleWalkaroundIndicator'; +export * from './CaptureSelection'; diff --git a/packages/common-ui-web/test/components/CaptureSelection.test.tsx b/packages/common-ui-web/test/components/CaptureSelection.test.tsx new file mode 100644 index 000000000..432c94856 --- /dev/null +++ b/packages/common-ui-web/test/components/CaptureSelection.test.tsx @@ -0,0 +1,44 @@ +import '@testing-library/jest-dom'; +import { render, fireEvent, screen } from '@testing-library/react'; +import { CaptureSelection } from '../../src'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe('CaptureSelection', () => { + it('renders correctly', () => { + const { unmount } = render(); + + expect(screen.getByText('addDamage.title')).toBeInTheDocument(); + expect(screen.getByText('addDamage.description')).toBeInTheDocument(); + expect(screen.getByText('addDamage.button')).toBeInTheDocument(); + expect(screen.getByText('capture.title')).toBeInTheDocument(); + expect(screen.getByText('capture.description')).toBeInTheDocument(); + expect(screen.getByText('capture.button')).toBeInTheDocument(); + + unmount(); + }); + + it('calls onAddDamage when "Add Damage" button is clicked', () => { + const onAddDamage = jest.fn(); + const { unmount } = render(); + + fireEvent.click(screen.getByText('addDamage.button')); + expect(onAddDamage).toHaveBeenCalled(); + + unmount(); + }); + + it('calls onCapture when "Take Picture" button is clicked', () => { + const onCapture = jest.fn(); + const { unmount } = render(); + + fireEvent.click(screen.getByText('capture.button')); + expect(onCapture).toHaveBeenCalled(); + + unmount(); + }); +}); diff --git a/packages/common/README.md b/packages/common/README.md index 69ac04519..6eb290935 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -18,6 +18,6 @@ you can refer to their own README directly : - [State Management](README/STATE_MANAGEMENT.md). - [Theming](README/THEMING.md). - [Internationalization](README/INTERNATIONALIZATION.md). -- [Hooks](README/APP_UTILS). +- [Hooks](README/APP_UTILS.md). - [Utilities](README/UTILITIES.md). -- [Application Utilities](README/HOOKS). +- [Application Utilities](README/HOOKS.md). diff --git a/packages/common/README/UTILITIES.md b/packages/common/README/UTILITIES.md index bb0488c2a..6f457cebf 100644 --- a/packages/common/README/UTILITIES.md +++ b/packages/common/README/UTILITIES.md @@ -45,6 +45,29 @@ method, available on all versions of JavaScript. --- +# Browser Utils +### isMobileDevice + +```typescript +import { isMobileDevice } from "@monkvision/common"; + +console.log(isMobileDevice()); +// Output : true or false +``` +Checks if the current device is a mobile device. + +### getAspectRatio +```typescript +import { getAspectRatio } from "@monkvision/common"; + +const streamDimensions = {width: 1920, height: 1080} +console.log(getAspectRatio(streamDimensions)); +// Output : '1920/1080' +``` +Returns the aspect ratio of the stream. If not a mobile device, it will return 16/9 by default. + +--- + # Color Utils ### getRGBAFromString ```typescript @@ -302,6 +325,19 @@ Converts a string to camel case. --- +# Vehicle +### getVehicleModel +```typescript +import { getVehicleModel } from '@monkvision/common' +import { VehicleType } from '@monkvision/types' + +console.log(getVehicleModel(VehicleType.SUV)) +output : 'fesc20' +``` +Returns the vehicle model corresponding to the given vehicle type. + +--- + # Zlib Utils ### zlibCompress ```typescript diff --git a/packages/common/src/utils/browser.utils.ts b/packages/common/src/utils/browser.utils.ts index d66e40934..09191512f 100644 --- a/packages/common/src/utils/browser.utils.ts +++ b/packages/common/src/utils/browser.utils.ts @@ -1,3 +1,5 @@ +import { PixelDimensions } from '@monkvision/types'; + /** * Checks if the current device is a mobile device. */ @@ -11,3 +13,13 @@ export function isMobileDevice(): boolean { userAgent.includes('windows phone') ); } + +/** + * Returns the aspect ratio of the stream. If not a mobile device, it will return 16/9 by default. + */ +export function getAspectRatio(streamDimensions?: PixelDimensions | null): string { + if (isMobileDevice() && streamDimensions) { + return `${streamDimensions?.width}/${streamDimensions?.height}`; + } + return '16/9'; +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index dd296ca28..4436e1dcb 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -9,3 +9,4 @@ export * from './env.utils'; export * from './state.utils'; export * from './config.utils'; export * from './formValidation.utils'; +export * from './vehicle.utils'; diff --git a/packages/common/src/utils/state.utils.ts b/packages/common/src/utils/state.utils.ts index 6568fa492..c9915d4e0 100644 --- a/packages/common/src/utils/state.utils.ts +++ b/packages/common/src/utils/state.utils.ts @@ -1,37 +1,43 @@ -import { Image } from '@monkvision/types'; +import { Image, ImageType } from '@monkvision/types'; /** * Utility function that extracts the images of the given inspection. * * @param inspectionId The ID of the inspection to get the images of. * @param images Array containing every image existing in the current local state. + * @param filterImageType The specific image type to filter by. If not provided, no type-based filtering is applied. * @param filterRetakes Boolean indicating if retaken pictures should be filtered out or not (default: false). */ export function getInspectionImages( inspectionId: string, images: Image[], + filterImageType?: ImageType, filterRetakes = false, ): Image[] { - const inspectionImages = images.filter((image) => image.inspectionId === inspectionId); + let inspectionImages = images.filter((image) => image.inspectionId === inspectionId); + + if (filterImageType) { + inspectionImages = inspectionImages.filter((image) => filterImageType === image.type); + } + if (!filterRetakes) { return inspectionImages; } - const filteredRetakes: Image[] = []; + + const filteredRetakes: Record = {}; inspectionImages.forEach((image) => { if (image.sightId) { - const index = filteredRetakes.findIndex((i) => i.sightId === image.sightId); - if (index >= 0) { - if ( - image.createdAt && - filteredRetakes[index].createdAt && - image.createdAt > (filteredRetakes[index].createdAt as number) - ) { - filteredRetakes[index] = image; - } - return; + const existingImage = filteredRetakes[image.sightId]; + if ( + !existingImage || + (image.createdAt && existingImage.createdAt && image.createdAt > existingImage.createdAt) + ) { + filteredRetakes[image.sightId] = image; } + } else { + filteredRetakes[image.id] = image; } - filteredRetakes.push(image); }); - return filteredRetakes; + + return Object.values(filteredRetakes); } diff --git a/packages/common/src/utils/vehicle.utils.ts b/packages/common/src/utils/vehicle.utils.ts new file mode 100644 index 000000000..455ed1ad9 --- /dev/null +++ b/packages/common/src/utils/vehicle.utils.ts @@ -0,0 +1,16 @@ +import { vehicles } from '@monkvision/sights'; +import { VehicleType, VehicleModel } from '@monkvision/types'; + +/** + * Returns the vehicle model corresponding to the given vehicle type. + */ +export function getVehicleModel(vehicleType: VehicleType): VehicleModel { + const ajustedVehicletype = vehicleType === VehicleType.SUV ? VehicleType.CUV : vehicleType; + const detail = Object.entries(vehicles) + .filter(([type]) => type !== VehicleModel.AUDIA7) + .find(([, details]) => details.type === ajustedVehicletype)?.[1]; + if (detail === undefined) { + throw new Error(`No vehicle model found for vehicle type ${ajustedVehicletype}`); + } + return detail.id; +} diff --git a/packages/common/test/utils/state.utils.test.ts b/packages/common/test/utils/state.utils.test.ts index 54d3d5e58..267ae7297 100644 --- a/packages/common/test/utils/state.utils.test.ts +++ b/packages/common/test/utils/state.utils.test.ts @@ -1,6 +1,48 @@ -import { Image } from '@monkvision/types'; +import { Image, ImageType } from '@monkvision/types'; import { getInspectionImages } from '../../src'; +const inspectionIdMock = 'test-inspection-1'; +const imagesMock = [ + { id: 'test-1', inspectionId: 'test-1', type: ImageType.CLOSE_UP }, + { + id: 'test-2', + inspectionId: inspectionIdMock, + sightId: 'sight-1', + createdAt: Date.parse('1998-01-01T01:01:01.001Z'), + type: ImageType.BEAUTY_SHOT, + }, + { + id: 'test-3', + inspectionId: inspectionIdMock, + sightId: 'sight-1', + createdAt: Date.parse('2020-01-01T01:01:01.001Z'), + type: ImageType.BEAUTY_SHOT, + }, + { + id: 'test-4', + inspectionId: inspectionIdMock, + sightId: 'sight-1', + createdAt: Date.parse('2024-01-01T01:01:01.001Z'), + type: ImageType.BEAUTY_SHOT, + }, + { id: 'test-5', inspectionId: inspectionIdMock, type: ImageType.CLOSE_UP }, + { id: 'test-6', inspectionId: inspectionIdMock, type: ImageType.CLOSE_UP }, + { + id: 'test-7', + inspectionId: inspectionIdMock, + sightId: 'sight-2', + createdAt: Date.parse('2024-01-01T01:01:01.001Z'), + type: ImageType.BEAUTY_SHOT, + }, + { + id: 'test-8', + inspectionId: inspectionIdMock, + sightId: 'sight-2', + createdAt: Date.parse('1998-01-01T01:01:01.001Z'), + type: ImageType.BEAUTY_SHOT, + }, +] as Image[]; + describe('State utils', () => { describe('getInspectionImages util function', () => { it('should return an empty array if there are no image in the inspection', () => { @@ -38,64 +80,39 @@ describe('State utils', () => { }); it('should properly filter retakes', () => { - const inspectionId = 'test-inspection-1'; - const images = [ - { id: 'test-1', inspectionId: 'test-1' }, - { - id: 'test-2', - inspectionId, - sightId: 'sight-1', - createdAt: Date.parse('1998-01-01T01:01:01.001Z'), - }, - { - id: 'test-3', - inspectionId, - sightId: 'sight-1', - createdAt: Date.parse('2020-01-01T01:01:01.001Z'), - }, - { - id: 'test-4', - inspectionId, - sightId: 'sight-1', - createdAt: Date.parse('2024-01-01T01:01:01.001Z'), - }, - { id: 'test-5', inspectionId }, - { id: 'test-6', inspectionId }, - { - id: 'test-7', - inspectionId, - sightId: 'sight-2', - createdAt: Date.parse('2024-01-01T01:01:01.001Z'), - }, - { - id: 'test-8', - inspectionId, - sightId: 'sight-2', - createdAt: Date.parse('1998-01-01T01:01:01.001Z'), - }, - ] as Image[]; - const inspectionImages = getInspectionImages(inspectionId, images, true); + const inspectionImages = getInspectionImages(inspectionIdMock, imagesMock, undefined, true); expect(inspectionImages.length).toBe(4); - expect(inspectionImages).toContainEqual({ - id: 'test-4', - inspectionId, - sightId: 'sight-1', - createdAt: Date.parse('2024-01-01T01:01:01.001Z'), - }); - expect(inspectionImages).toContainEqual({ - id: 'test-5', - inspectionId, - }); - expect(inspectionImages).toContainEqual({ - id: 'test-6', - inspectionId, - }); - expect(inspectionImages).toContainEqual({ - id: 'test-7', - inspectionId, - sightId: 'sight-2', - createdAt: Date.parse('2024-01-01T01:01:01.001Z'), - }); + expect(inspectionImages).toContainEqual(imagesMock.at(3)); + expect(inspectionImages).toContainEqual(imagesMock.at(4)); + expect(inspectionImages).toContainEqual(imagesMock.at(5)); + expect(inspectionImages).toContainEqual(imagesMock.at(6)); + }); + + it('should properly filter image type by Image.CLOSE_UP', () => { + const inspectionImages = getInspectionImages( + inspectionIdMock, + imagesMock, + ImageType.CLOSE_UP, + false, + ); + expect(inspectionImages.length).toBe(2); + expect(inspectionImages).toContainEqual(imagesMock.at(4)); + expect(inspectionImages).toContainEqual(imagesMock.at(5)); + }); + + it('should properly filter image type by Image.BEAUTY_SHOT', () => { + const inspectionImages = getInspectionImages( + inspectionIdMock, + imagesMock, + ImageType.BEAUTY_SHOT, + false, + ); + expect(inspectionImages.length).toBe(5); + expect(inspectionImages).toContainEqual(imagesMock.at(1)); + expect(inspectionImages).toContainEqual(imagesMock.at(2)); + expect(inspectionImages).toContainEqual(imagesMock.at(3)); + expect(inspectionImages).toContainEqual(imagesMock.at(6)); + expect(inspectionImages).toContainEqual(imagesMock.at(7)); }); }); }); diff --git a/packages/common/test/utils/vehicle.utils.test.ts b/packages/common/test/utils/vehicle.utils.test.ts new file mode 100644 index 000000000..463bef0ba --- /dev/null +++ b/packages/common/test/utils/vehicle.utils.test.ts @@ -0,0 +1,29 @@ +import { VehicleModel, VehicleType } from '@monkvision/types'; +import { getVehicleModel } from '../../src'; + +describe('getVehicleModel', () => { + it('should return the correct vehicle model for a given vehicle type', () => { + const vehicleType = VehicleType.PICKUP; + const expectedModel = VehicleModel.FF150; + expect(getVehicleModel(vehicleType)).toBe(expectedModel); + }); + + it('should return the correct vehicle model for SUV type', () => { + const vehicleType = VehicleType.SUV; + const expectedModel = VehicleModel.FESC20; + expect(getVehicleModel(vehicleType)).toBe(expectedModel); + }); + + it('should return the correct vehicle model for CUV type', () => { + const vehicleType = VehicleType.CUV; + const expectedModel = VehicleModel.FESC20; + expect(getVehicleModel(vehicleType)).toBe(expectedModel); + }); + + it('should throw an error if no vehicle model is found', () => { + const vehicleType = VehicleType.HATCHBACK; + expect(() => getVehicleModel(vehicleType)).toThrowError( + `No vehicle model found for vehicle type ${vehicleType}`, + ); + }); +}); diff --git a/packages/inspection-capture-web/README.md b/packages/inspection-capture-web/README.md index 653223654..b037d1cf7 100644 --- a/packages/inspection-capture-web/README.md +++ b/packages/inspection-capture-web/README.md @@ -5,6 +5,9 @@ There are two main workflows for capturing pictures of a vehicle for a Monk insp to take pictures of the vehicle by aligning the vehicle with the Sight overlays. - The **VideoCapture** workflow : the user is asked to record a quick video of their vehicle by filming it and rotating in a full circle around it. +- The **DamageDisclosure** workflow : The user is guided to capture close-up pictures of specific damaged parts of the vehicle. There are 2 workflows available: + - Part-selection: Before taking the picture, the user must first select the damaged part on the vehicle wireframe then a close-up picture of the damage. + - Two-shot: The user is asked to take, first a wide picture of the vehicle, then a close-up picture of the damage. # Installing To install the package, you can run the following command : @@ -95,6 +98,7 @@ export function MonkPhotoCapturePage({ authToken }) { | validateButtonLabel | `string` | Custom label for validate button in gallery view. | | | | maxUploadDurationWarning | `number` | Max upload duration in milliseconds before showing a bad connection warning to the user. Use `-1` to never display the warning. | | `15000` | | useAdaptiveImageQuality | `boolean` | Boolean indicating if the image quality should be downgraded automatically in case of low connection. | | `true` | +| vehicleType | `VehicleType` | The vehicle type of the inspection. | | `VehicleType.SEDAN` | # VideoCapture The VideoCapture workflow is aimed at asking users to record a walkaround video of their vehicle (around ~1min per @@ -130,6 +134,8 @@ export function MonkVideoCapturePage({ authToken }) { } ``` +Props + | Prop | Type | Description | Required | Default Value | |------------------------------|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------------| | format | `CompressionFormat` | The output format of the compression. | | `CompressionFormat.JPEG` | @@ -149,3 +155,58 @@ export function MonkVideoCapturePage({ authToken }) { | enablePhoneShakingWarning | `boolean` | Boolean indicating if a warning should be shown to the user when they are shaking their phone too much. | | `true` | | fastWalkingWarningCooldown | `number` | The duration (in milliseconds) to wait between fast walking warnings. | | `4000` | | phoneShakingWarningCooldown | `number` | The duration (in milliseconds) to wait between phone shaking warnings. | | `4000` | + +# DamageDisclosure + +The DamageDisclosure workflow is designed to guide users in documenting and disclosing damage to their vehicles during a Monk inspection. +This workflow is ideal for capturing detailed images of specific damages such as dents, scratches, or other issues that need to be highlighted in the inspection report. +There are 2 workflows available. + +Please refer to the [official MonkJs documentation](https://monkvision.github.io/monkjs/docs/photo-capture-workflow) for a comprehensive overview of the Add damage workflow. + +## DamageDisclosure component + +This package exports a ready-to-use single-page component called DamageDisclosure that implements the DamageDisclosure workflow. You can integrate it into your application by creating a new page containing only this component. Before using it, you must generate an Auth0 authentication token and create a new inspection using the Monk API. Ensure that all task statuses in the inspection are set to NOT_STARTED. + +You can then pass the inspection ID, API configuration (including the auth token). Once the user completes the workflow, the onComplete callback is triggered, allowing you to navigate to another page or perform additional actions. + +The following example demonstrates how to use the DamageDisclosure component: + +```tsx +import { DamageDisclosure } from '@monkvision/inspection-capture-web'; + +const apiDomain = 'api.preview.monk.ai/v1'; + +export function MonkDamageDisclosurePage({ authToken }) { + return ( + { /* Navigate to another page */ }} + /> + ); +} +``` + +Props + +| Prop | Type | Description | Required | Default Value | +|----------------------------|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------------------------| +| inspectionId | string | The ID of the inspection to add images to. Make sure that the user that created the inspection if the same one as the one described in the auth token in the `apiConfig` prop. | ✔️ | | +| apiConfig | ApiConfig | The api config used to communicate with the API. Make sure that the user described in the auth token is the same one as the one that created the inspection provided in the `inspectionId` prop. | ✔️ | | +| onClose | `() => void` | Callback called when the user clicks on the Close button. If this callback is not provided, the button will not be displayed on the screen. | | | +| onComplete | `() => void` | Callback called when inspection capture is complete. | | | +| onPictureTaken | `(picture: MonkPicture) => void` | Callback called when the user has taken a picture in the Capture process. | | | +| lang | string | null | The language to be used by this component. | | `'en'` | +| enforceOrientation | `DeviceOrientation` | Use this prop to enforce a specific device orientation for the Camera screen. | | | +| maxUploadDurationWarning | `number` | Max upload duration in milliseconds before showing a bad connection warning to the user. Use `-1` to never display the warning. | | `15000` | +| useAdaptiveImageQuality | `boolean` | Boolean indicating if the image quality should be downgraded automatically in case of low connection. | | `true` | +| showCloseButton | `boolean` | Indicates if the close button should be displayed in the HUD on top of the Camera preview. | | `false` | +| format | `CompressionFormat` | The output format of the compression. | | `CompressionFormat.JPEG` | +| quality | `number` | Value indicating image quality for the compression output. | | `0.6` | +| resolution | `CameraResolution` | Indicates the resolution of the pictures taken by the Camera. | | `CameraResolution.UHD_4K` | +| allowImageUpscaling | `boolean` | Allow images to be scaled up if the device does not support the specified resolution in the `resolution` prop. | | `false` | +| useLiveCompliance | `boolean` | Indicates if live compliance should be enabled or not. | | `false` | +| validateButtonLabel | `string` | Custom label for validate button in gallery view. | | | +| thumbnailDomain | `string` | The API domain used to communicate with the resize micro service. | ✔️ | | +| vehicleType | `VehicleType` | The vehicle type of the inspection. | | `VehicleType.SEDAN` | diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosure.styles.ts b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosure.styles.ts new file mode 100644 index 000000000..64345479c --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosure.styles.ts @@ -0,0 +1,32 @@ +import { Styles } from '@monkvision/types'; + +export const styles: Styles = { + container: { + height: '100%', + width: '100%', + }, + orientationErrorContainer: { + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + boxSizing: 'border-box', + padding: '50px 10%', + }, + orientationErrorTitleContainer: { + display: 'flex', + alignItems: 'center', + }, + orientationErrorTitle: { + fontSize: 18, + marginLeft: 16, + }, + orientationErrorDescription: { + fontSize: 16, + paddingTop: 16, + opacity: 0.8, + textAlign: 'center', + }, +}; diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosure.tsx b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosure.tsx new file mode 100644 index 000000000..4b22c8b32 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosure.tsx @@ -0,0 +1,235 @@ +import { useAnalytics } from '@monkvision/analytics'; +import { Camera, CameraHUDProps, CameraProps } from '@monkvision/camera-web'; +import { + useI18nSync, + useLoadingState, + useObjectMemo, + useWindowDimensions, +} from '@monkvision/common'; +import { BackdropDialog, Icon, InspectionGallery } from '@monkvision/common-ui-web'; +import { MonkApiConfig } from '@monkvision/network'; +import { + AddDamage, + CameraConfig, + PhotoCaptureAppConfig, + ComplianceOptions, + CompressionOptions, + DeviceOrientation, + ImageType, + MonkPicture, + VehicleType, +} from '@monkvision/types'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { styles } from './DamageDisclosure.styles'; +import { DamageDisclosureHUD, DamageDisclosureHUDProps } from './DamageDisclosureHUD'; +import { useDamageDisclosureState } from './hooks'; +import { + useAdaptiveCameraConfig, + useAddDamageMode, + usePhotoCaptureImages, + usePictureTaken, + useUploadQueue, + useBadConnectionWarning, + useTracking, +} from '../hooks'; +import { CaptureScreen } from '../types'; + +/** + * Props of the DamageDisclosure component. + */ +export interface DamageDisclosureProps + extends Pick, 'resolution' | 'allowImageUpscaling'>, + Pick< + PhotoCaptureAppConfig, + | keyof CameraConfig + | 'maxUploadDurationWarning' + | 'useAdaptiveImageQuality' + | 'showCloseButton' + | 'enforceOrientation' + | 'addDamage' + >, + Partial, + Partial { + /** + * The ID of the inspection to add images to. Make sure that the user that created the inspection if the same one as + * the one described in the auth token in the `apiConfig` prop. + */ + inspectionId: string; + /** + * The api config used to communicate with the API. Make sure that the user described in the auth token is the same + * one as the one that created the inspection provided in the `inspectionId` prop. + */ + apiConfig: MonkApiConfig; + /** + * The vehicle type of the inspection. + */ + vehicleType?: VehicleType; + /** + * Callback called when the user clicks on the Close button. If this callback is not provided, the button will not be + * displayed on the screen. + */ + onClose?: () => void; + /** + * Callback called when damage disclosure is complete. + */ + onComplete?: () => void; + /** + * Callback called when a picture has been taken by the user. + */ + onPictureTaken?: (picture: MonkPicture) => void; + /** + * The language to be used by this component. + * + * @default en + */ + lang?: string | null; +} + +// No ts-doc for this component : the component exported is DamageDisclosureHOC +export function DamageDisclosure({ + inspectionId, + apiConfig, + onClose, + onComplete, + onPictureTaken, + enableCompliance = true, + complianceIssues, + customComplianceThresholds, + useLiveCompliance = false, + maxUploadDurationWarning = 15000, + showCloseButton = false, + addDamage = AddDamage.PART_SELECT, + useAdaptiveImageQuality = true, + lang, + enforceOrientation, + vehicleType = VehicleType.SEDAN, + ...initialCameraConfig +}: DamageDisclosureProps) { + useI18nSync(lang); + const complianceOptions: ComplianceOptions = useObjectMemo({ + enableCompliance, + complianceIssues, + useLiveCompliance, + customComplianceThresholds, + }); + const { t } = useTranslation(); + const [currentScreen, setCurrentScreen] = useState(CaptureScreen.CAMERA); + const dimensions = useWindowDimensions(); + const analytics = useAnalytics(); + const loading = useLoadingState(); + const handleOpenGallery = () => { + setCurrentScreen(CaptureScreen.GALLERY); + analytics.trackEvent('Gallery Opened'); + }; + const addDamageHandle = useAddDamageMode({ + addDamage, + currentScreen, + damageDisclosure: true, + handleOpenGallery, + }); + const disclosureState = useDamageDisclosureState({ + inspectionId, + apiConfig, + loading, + complianceOptions, + }); + useTracking({ inspectionId, authToken: apiConfig.authToken }); + const { adaptiveCameraConfig, uploadEventHandlers: adaptiveUploadEventHandlers } = + useAdaptiveCameraConfig({ + initialCameraConfig, + useAdaptiveImageQuality, + }); + const { + isBadConnectionWarningDialogDisplayed, + closeBadConnectionWarningDialog, + uploadEventHandlers: badConnectionWarningUploadEventHandlers, + } = useBadConnectionWarning({ maxUploadDurationWarning }); + const uploadQueue = useUploadQueue({ + inspectionId, + apiConfig, + complianceOptions, + eventHandlers: [adaptiveUploadEventHandlers, badConnectionWarningUploadEventHandlers], + }); + const images = usePhotoCaptureImages(inspectionId); + const handlePictureTaken = usePictureTaken({ + captureState: disclosureState, + addDamageHandle, + uploadQueue, + onPictureTaken, + }); + const handleGalleryBack = () => { + setCurrentScreen(CaptureScreen.CAMERA); + }; + const isViolatingEnforcedOrientation = + enforceOrientation && + (enforceOrientation === DeviceOrientation.PORTRAIT) !== dimensions.isPortrait; + const hudProps: Omit = { + inspectionId, + mode: addDamageHandle.mode, + vehicleParts: addDamageHandle.vehicleParts, + lastPictureTakenUri: disclosureState.lastPictureTakenUri, + onOpenGallery: handleOpenGallery, + onAddDamage: addDamageHandle.handleAddDamage, + onAddDamagePartsSelected: addDamageHandle.handleAddDamagePartsSelected, + onCancelAddDamage: addDamageHandle.handleCancelAddDamage, + onRetry: disclosureState.retryLoadingInspection, + loading, + onClose, + showCloseButton, + images, + addDamage, + onValidateVehicleParts: addDamageHandle.handleValidateVehicleParts, + vehicleType, + }; + + return ( +
+ {currentScreen === CaptureScreen.CAMERA && isViolatingEnforcedOrientation && ( +
+
+ +
{t('photo.orientationError.title')}
+
+
+ {t('photo.orientationError.description')} +
+
+ )} + {currentScreen === CaptureScreen.CAMERA && !isViolatingEnforcedOrientation && ( + + )} + {currentScreen === CaptureScreen.GALLERY && ( + + )} + +
+ ); +} diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHOC.tsx b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHOC.tsx new file mode 100644 index 000000000..513ac10a5 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHOC.tsx @@ -0,0 +1,38 @@ +import { i18nWrap, MonkProvider } from '@monkvision/common'; +import { i18nInspectionCaptureWeb } from '../i18n'; +import { DamageDisclosure, DamageDisclosureProps } from './DamageDisclosure'; + +/** + * The DamageDisclosure component is a ready-to-use, single page component that implements a Camera app, allowing users + * to capture photos of damaged parts of their vehicle for the purpose of disclosing damage. In order to use this + * component, you first need to generate an Auth0 authentication token, and create an inspection using the Monk Api. + * When creating the inspection, don't forget to set the tasks statuses to `NOT_STARTED`. You can then pass the + * inspection ID, the api config (with the auth token) and everything will be handled automatically for you. + * + * @example + * import { DamageDisclosure } from '@monkvision/inspection-capture-web'; + * + * export function PhotoCaptureScreen({ inspectionId, apiConfig }: PhotoCaptureScreenProps) { + * const { i18n } = useTranslation(); + * + * return ( + * { / * Navigate to another page * / }} + * lang={i18n.language} + * /> + * ); + * } + */ +export const DamageDisclosureHOC = i18nWrap(function DamageDisclosureHOC( + props: DamageDisclosureProps, +) { + return ( + + + + ); +}, +i18nInspectionCaptureWeb); diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD.styles.ts b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD.styles.ts new file mode 100644 index 000000000..3e94800ea --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD.styles.ts @@ -0,0 +1,23 @@ +import { Styles } from '@monkvision/types'; + +export const styles: Styles = { + container: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + position: 'relative', + alignSelf: 'stretch', + }, + containerPortrait: { + __media: { portrait: true }, + flexDirection: 'column', + }, + previewContainer: { + position: 'relative', + width: '100%', + height: '100%', + }, +}; diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD.tsx b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD.tsx new file mode 100644 index 000000000..8c5373440 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD.tsx @@ -0,0 +1,184 @@ +import { useMemo, useState } from 'react'; +import { + PhotoCaptureAppConfig, + Image, + ImageStatus, + ImageType, + VehiclePart, + VehicleType, +} from '@monkvision/types'; +import { useTranslation } from 'react-i18next'; +import { BackdropDialog } from '@monkvision/common-ui-web'; +import { CameraHUDProps } from '@monkvision/camera-web'; +import { LoadingState } from '@monkvision/common'; +import { useAnalytics } from '@monkvision/analytics'; +import { styles } from './DamageDisclosureHUD.styles'; +import { CaptureMode } from '../../types'; +import { HUDButtons } from '../../components/HUDButtons'; +import { DamageDisclosureHUDElements } from './DamageDisclosureHUDElements'; +import { HUDOverlay } from '../../components/HUDOverlay'; + +/** + * Props of the DamageDisclosureHUD component. + */ +export interface DamageDisclosureHUDProps + extends CameraHUDProps, + Pick { + /** + * The inspection ID. + */ + inspectionId: string; + /** + * The current mode of the component. + */ + mode: CaptureMode; + /** + * Global loading state of the DamageDisclosure component. + */ + loading: LoadingState; + /** + * Current vehicle parts selected to take a picture of. + */ + vehicleParts: VehiclePart[]; + /** + * Value storing the last picture taken by the user. If no picture has been taken yet, this value is null. + */ + lastPictureTakenUri: string | null; + /** + * Callback called when the user clicks on the "Add Damage" button. + */ + onAddDamage: () => void; + /** + * Callback called when the user selects the parts to take a picture of. + */ + onAddDamagePartsSelected?: (parts: VehiclePart[]) => void; + /** + * Callback called when the user clicks on the "Cancel" button of the Add Damage mode. + */ + onCancelAddDamage: () => void; + /** + * Callback that can be used to retry fetching this state object from the API in case the previous fetch failed. + */ + onRetry: () => void; + /** + * Callback called when the user clicks on the gallery icon. + */ + onOpenGallery: () => void; + /** + * Callback called when the user clicks on the "Validate" button of the Add Damage mode. + */ + onValidateVehicleParts: () => void; + /** + * Callback called when the user clicks on the close button. If this callback is not provided, the close button is not + * displayed. + */ + onClose?: () => void; + /** + * The current images taken by the user (ignoring retaken pictures etc.). + */ + images: Image[]; + /** + * The vehicle type of the inspection. + */ + vehicleType: VehicleType; +} + +/** + * This component implements the Camera HUD (head-up display) displayed in the DamageDisclosure component + * over the Camera preview. It implements elements such as buttons to interact with + * the camera, DamageDisclosure indicators, error messages, loaders etc. + */ +export function DamageDisclosureHUD({ + inspectionId, + lastPictureTakenUri, + mode, + vehicleParts, + addDamage, + onAddDamage, + onAddDamagePartsSelected, + onValidateVehicleParts, + onCancelAddDamage, + onOpenGallery, + onClose, + onRetry, + showCloseButton, + loading, + handle, + cameraPreview, + images, + vehicleType, +}: DamageDisclosureHUDProps) { + const { t } = useTranslation(); + const [showCloseModal, setShowCloseModal] = useState(false); + const { trackEvent } = useAnalytics(); + const retakeCount = useMemo( + () => + images.filter( + (image) => + [ImageStatus.NOT_COMPLIANT, ImageStatus.UPLOAD_FAILED, ImageStatus.UPLOAD_ERROR].includes( + image.status, + ) && image.type === ImageType.CLOSE_UP, + ).length, + [images], + ); + + const handleCloseConfirm = () => { + setShowCloseModal(false); + trackEvent('Disclosure Closed'); + onClose?.(); + }; + + return ( +
+
+ {cameraPreview} + { + + } +
+ {mode !== CaptureMode.ADD_DAMAGE_PART_SELECT && ( + setShowCloseModal(true)} + galleryPreview={lastPictureTakenUri ?? undefined} + closeDisabled={!!loading.error || !!handle.error} + galleryDisabled={!!loading.error || !!handle.error} + takePictureDisabled={ + !!loading.error || !!handle.error || handle.isLoading || loading.isLoading + } + showCloseButton={showCloseButton} + showGalleryBadge={retakeCount > 0} + retakeCount={retakeCount} + /> + )} + + setShowCloseModal(false)} + onConfirm={handleCloseConfirm} + /> +
+ ); +} diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements/DamageDisclosureHUDElements.tsx b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements/DamageDisclosureHUDElements.tsx new file mode 100644 index 000000000..d41b4b1b1 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements/DamageDisclosureHUDElements.tsx @@ -0,0 +1,85 @@ +import { + PhotoCaptureAppConfig, + PixelDimensions, + VehiclePart, + VehicleType, +} from '@monkvision/types'; +import { CaptureMode } from '../../../types'; +import { CloseUpShot, PartSelection, ZoomOutShot } from '../../../components'; +/** + * Props of the DamageDisclosureHUDElements component. + */ +export interface DamageDisclosureHUDElementsProps extends Pick { + /** + * The current mode of the component. + */ + mode: CaptureMode; + /** + * Current vehicle parts selected to take a picture of. + */ + vehicleParts: VehiclePart[]; + /** + * Callback called when the user presses the Add Damage button. + */ + onAddDamage: () => void; + /** + * Callback called when the user selects the parts to take a picture of. + */ + onAddDamagePartsSelected?: (parts: VehiclePart[]) => void; + /** + * Callback called when the user cancels the Add Damage mode. + */ + onCancelAddDamage: () => void; + /** + * Callback called when the user clicks on the "Validate" button of the Add Damage mode. + */ + onValidateVehicleParts: () => void; + /** + * The effective pixel dimensions of the Camera video stream on the screen. + */ + previewDimensions: PixelDimensions | null; + /** + * Boolean indicating if the global loading state of the DamageDisclosure component is loading or not. + */ + isLoading?: boolean; + /** + * The error that occurred in the DamageDisclosure component. Set this value to `null` if there is no error. + */ + error?: unknown | null; + /** + * The vehicle type of the inspection. + */ + vehicleType: VehicleType; +} + +/** + * Component implementing an HUD displayed on top of the Camera preview during the DamageDisclosure process. + */ +export function DamageDisclosureHUDElements(params: DamageDisclosureHUDElementsProps) { + if (params.isLoading || !!params.error) { + return null; + } + if (params.mode === CaptureMode.ADD_DAMAGE_1ST_SHOT) { + return ; + } + if ( + [CaptureMode.ADD_DAMAGE_2ND_SHOT, CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT].includes(params.mode) + ) { + return ( + + ); + } + return ( + + ); +} diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements/index.ts b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements/index.ts new file mode 100644 index 000000000..012d4a0e9 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements/index.ts @@ -0,0 +1,4 @@ +export { + DamageDisclosureHUDElements, + type DamageDisclosureHUDElementsProps, +} from './DamageDisclosureHUDElements'; diff --git a/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/index.ts b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/index.ts new file mode 100644 index 000000000..c326515de --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/DamageDisclosureHUD/index.ts @@ -0,0 +1,2 @@ +export { DamageDisclosureHUD, type DamageDisclosureHUDProps } from './DamageDisclosureHUD'; +export * from './DamageDisclosureHUDElements'; diff --git a/packages/inspection-capture-web/src/DamageDisclosure/hooks/index.ts b/packages/inspection-capture-web/src/DamageDisclosure/hooks/index.ts new file mode 100644 index 000000000..b36c9ecf9 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/hooks/index.ts @@ -0,0 +1 @@ +export * from './useDamageDisclosureState'; diff --git a/packages/inspection-capture-web/src/DamageDisclosure/hooks/useDamageDisclosureState.ts b/packages/inspection-capture-web/src/DamageDisclosure/hooks/useDamageDisclosureState.ts new file mode 100644 index 000000000..7ac5046ad --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/hooks/useDamageDisclosureState.ts @@ -0,0 +1,97 @@ +import { useCallback, useState } from 'react'; +import { + GetInspectionResponse, + MonkApiConfig, + MonkApiResponse, + useMonkApi, +} from '@monkvision/network'; +import { useMonitoring } from '@monkvision/monitoring'; +import { LoadingState, useAsyncEffect, useObjectMemo } from '@monkvision/common'; +import { ComplianceOptions, Image, ImageType } from '@monkvision/types'; +import { DamageDisclosureState } from '../../types'; + +/** + * Parameters of the useDamageDisclosureState hook. + */ +export interface DamageDisclosureParams { + /** + * The inspection ID. + */ + inspectionId: string; + /** + * The api config used to communicate with the API. + */ + apiConfig: MonkApiConfig; + /** + * Global loading state of the DamageDisclosure component. + */ + loading: LoadingState; + /** + * The options for the compliance conf + */ + complianceOptions: ComplianceOptions; +} + +function getLastPictureTakenUri( + inspectionId: string, + response: MonkApiResponse, +): string | null { + const images = response.entities.images.filter( + (image: Image) => image.inspectionId === inspectionId && image.type === ImageType.CLOSE_UP, + ); + return images && images.length > 0 ? images[images.length - 1].path : null; +} + +/** + * Custom hook used to manage the state of the DamageDisclosure. This state is automatically fetched from the API at + * the start of the DamageDisclosure process in order to allow users to start the inspection where they left it before. + */ +export function useDamageDisclosureState({ + inspectionId, + apiConfig, + loading, + complianceOptions, +}: DamageDisclosureParams): DamageDisclosureState { + const [retryCount, setRetryCount] = useState(0); + const [lastPictureTakenUri, setLastPictureTakenUri] = useState(null); + const { getInspection } = useMonkApi(apiConfig); + const { handleError } = useMonitoring(); + + const onFetchInspection = (response: MonkApiResponse) => { + try { + setLastPictureTakenUri(getLastPictureTakenUri(inspectionId, response)); + loading.onSuccess(); + } catch (err) { + handleError(err); + loading.onError(err); + } + }; + + useAsyncEffect( + () => { + loading.start(); + return getInspection({ + id: inspectionId, + compliance: complianceOptions, + }); + }, + [inspectionId, retryCount, complianceOptions], + { + onResolve: onFetchInspection, + onReject: (err) => { + handleError(err); + loading.onError(err); + }, + }, + ); + + const retryLoadingInspection = useCallback(() => { + setRetryCount((value) => value + 1); + }, []); + + return useObjectMemo({ + lastPictureTakenUri, + setLastPictureTakenUri, + retryLoadingInspection, + }); +} diff --git a/packages/inspection-capture-web/src/DamageDisclosure/index.ts b/packages/inspection-capture-web/src/DamageDisclosure/index.ts new file mode 100644 index 000000000..ccd5868f9 --- /dev/null +++ b/packages/inspection-capture-web/src/DamageDisclosure/index.ts @@ -0,0 +1,3 @@ +export * from './DamageDisclosureHUD'; +export { type DamageDisclosureProps } from './DamageDisclosure'; +export { DamageDisclosureHOC as DamageDisclosure } from './DamageDisclosureHOC'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx index 7f156294f..12f99621e 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx @@ -10,29 +10,33 @@ import { import { useMonitoring } from '@monkvision/monitoring'; import { MonkApiConfig } from '@monkvision/network'; import { + AddDamage, CameraConfig, ComplianceOptions, MonkPicture, PhotoCaptureAppConfig, PhotoCaptureTutorialOption, Sight, + VehicleType, } from '@monkvision/types'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { styles } from './PhotoCapture.styles'; import { PhotoCaptureHUD, PhotoCaptureHUDProps } from './PhotoCaptureHUD'; -import { useStartTasksOnComplete } from '../hooks'; import { - useAdaptiveCameraConfig, + useStartTasksOnComplete, + usePictureTaken, useAddDamageMode, + useUploadQueue, + usePhotoCaptureImages, + useAdaptiveCameraConfig, useBadConnectionWarning, + useTracking, +} from '../hooks'; +import { useComplianceAnalytics, - usePhotoCaptureImages, usePhotoCaptureSightState, usePhotoCaptureTutorial, - usePictureTaken, - useTracking, - useUploadQueue, } from './hooks'; /** @@ -50,7 +54,7 @@ export interface PhotoCaptureProps | 'showCloseButton' | 'enforceOrientation' | 'allowSkipRetake' - | 'enableAddDamage' + | 'addDamage' | 'sightGuidelines' | 'enableSightGuidelines' | 'enableTutorial' @@ -73,6 +77,10 @@ export interface PhotoCaptureProps * one as the one that created the inspection provided in the `inspectionId` prop. */ apiConfig: MonkApiConfig; + /** + * The vehicle type of the inspection. + */ + vehicleType?: VehicleType; /** * Callback called when the user clicks on the Close button. If this callback is not provided, the button will not be * displayed on the screen. @@ -124,7 +132,7 @@ export function PhotoCapture({ customComplianceThresholdsPerSight, useLiveCompliance = false, allowSkipRetake = false, - enableAddDamage = true, + addDamage = AddDamage.PART_SELECT, sightGuidelines, enableTutorial = PhotoCaptureTutorialOption.FIRST_TIME_ONLY, allowSkipTutorial = true, @@ -134,6 +142,7 @@ export function PhotoCapture({ lang, enforceOrientation, validateButtonLabel, + vehicleType = VehicleType.SEDAN, ...initialCameraConfig }: PhotoCaptureProps) { useI18nSync(lang); @@ -151,7 +160,14 @@ export function PhotoCapture({ const [currentScreen, setCurrentScreen] = useState(PhotoCaptureScreen.CAMERA); const analytics = useAnalytics(); const loading = useLoadingState(); - const addDamageHandle = useAddDamageMode(); + const handleOpenGallery = () => { + setCurrentScreen(PhotoCaptureScreen.GALLERY); + analytics.trackEvent('Gallery Opened'); + }; + const addDamageHandle = useAddDamageMode({ + addDamage, + handleOpenGallery, + }); useTracking({ inspectionId, authToken: apiConfig.authToken }); const { setIsInitialInspectionFetched } = useComplianceAnalytics({ inspectionId, sights }); const { adaptiveCameraConfig, uploadEventHandlers: adaptiveUploadEventHandlers } = @@ -200,16 +216,12 @@ export function PhotoCapture({ }); const images = usePhotoCaptureImages(inspectionId); const handlePictureTaken = usePictureTaken({ - sightState, + captureState: sightState, addDamageHandle, uploadQueue, tasksBySight, onPictureTaken, }); - const handleOpenGallery = () => { - setCurrentScreen(PhotoCaptureScreen.GALLERY); - analytics.trackEvent('Gallery Opened'); - }; const handleGalleryBack = () => setCurrentScreen(PhotoCaptureScreen.CAMERA); const handleNavigateToCapture = (options: NavigateToCaptureOptions) => { if (options.reason === NavigateToCaptureReason.ADD_DAMAGE) { @@ -249,10 +261,12 @@ export function PhotoCapture({ sightsTaken: sightState.sightsTaken, lastPictureTakenUri: sightState.lastPictureTakenUri, mode: addDamageHandle.mode, + vehicleParts: addDamageHandle.vehicleParts, onOpenGallery: handleOpenGallery, onSelectSight: sightState.selectSight, onRetakeSight: sightState.retakeSight, onAddDamage: addDamageHandle.handleAddDamage, + onAddDamagePartsSelected: addDamageHandle.handleAddDamagePartsSelected, onCancelAddDamage: addDamageHandle.handleCancelAddDamage, onRetry: sightState.retryLoadingInspection, loading, @@ -260,7 +274,8 @@ export function PhotoCapture({ inspectionId, showCloseButton, images, - enableAddDamage, + addDamage, + onValidateVehicleParts: addDamageHandle.handleValidateVehicleParts, sightGuidelines, enableSightGuidelines, currentTutorialStep, @@ -268,6 +283,7 @@ export function PhotoCapture({ onCloseTutorial: closeTutorial, allowSkipTutorial, enforceOrientation, + vehicleType, }; return ( @@ -292,7 +308,7 @@ export function PhotoCapture({ onBack={handleGalleryBack} onNavigateToCapture={handleNavigateToCapture} onValidate={handleGalleryValidate} - enableAddDamage={enableAddDamage} + addDamage={addDamage} validateButtonLabel={validateButtonLabel} isInspectionCompleted={sightState.isInspectionCompleted} {...complianceOptions} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx index 7ace867c3..3ae301b2f 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx @@ -1,17 +1,23 @@ import { useMemo, useState } from 'react'; -import { PhotoCaptureAppConfig, Image, ImageStatus, Sight } from '@monkvision/types'; +import { + PhotoCaptureAppConfig, + Image, + ImageStatus, + Sight, + VehiclePart, + VehicleType, +} from '@monkvision/types'; import { useTranslation } from 'react-i18next'; import { BackdropDialog } from '@monkvision/common-ui-web'; import { CameraHUDProps } from '@monkvision/camera-web'; import { LoadingState } from '@monkvision/common'; import { useAnalytics } from '@monkvision/analytics'; -import { PhotoCaptureHUDButtons } from './PhotoCaptureHUDButtons'; import { usePhotoCaptureHUDStyle } from './hooks'; -import { PhotoCaptureMode, TutorialSteps } from '../hooks'; -import { PhotoCaptureHUDOverlay } from './PhotoCaptureHUDOverlay'; +import { TutorialSteps } from '../hooks'; import { PhotoCaptureHUDElements } from './PhotoCaptureHUDElements'; import { PhotoCaptureHUDTutorial } from './PhotoCaptureHUDTutorial'; -import { OrientationEnforcer } from '../../components'; +import { CaptureMode } from '../../types'; +import { HUDButtons, HUDOverlay, OrientationEnforcer } from '../../components'; /** * Props of the PhotoCaptureHUD component. @@ -22,7 +28,7 @@ export interface PhotoCaptureHUDProps PhotoCaptureAppConfig, | 'enableSightGuidelines' | 'sightGuidelines' - | 'enableAddDamage' + | 'addDamage' | 'showCloseButton' | 'allowSkipTutorial' | 'enforceOrientation' @@ -50,7 +56,7 @@ export interface PhotoCaptureHUDProps /** * The current mode of the component. */ - mode: PhotoCaptureMode; + mode: CaptureMode; /** * Global loading state of the PhotoCapture component. */ @@ -59,6 +65,10 @@ export interface PhotoCaptureHUDProps * The current tutorial step in PhotoCapture component. */ currentTutorialStep: TutorialSteps | null; + /** + * Current vehicle parts selected to take a picture of. + */ + vehicleParts: VehiclePart[]; /** * Callback called when the user clicks on "Next" button in PhotoCapture tutorial. */ @@ -76,11 +86,15 @@ export interface PhotoCaptureHUDProps */ onRetakeSight: (sight: string) => void; /** - * Callback to be called when the user clicks on the "Add Damage" button. + * Callback called when the user clicks on the "Add Damage" button. */ onAddDamage: () => void; /** - * Callback to be called when the user clicks on the "Cancel" button of the Add Damage mode. + * Callback called when the user selects the parts to take a picture of. + */ + onAddDamagePartsSelected?: (parts: VehiclePart[]) => void; + /** + * Callback called when the user clicks on the "Cancel" button of the Add Damage mode. */ onCancelAddDamage: () => void; /** @@ -91,6 +105,10 @@ export interface PhotoCaptureHUDProps * Callback called when the user clicks on the gallery icon. */ onOpenGallery: () => void; + /** + * Callback called when the user clicks on the "Validate" button of the Add Damage mode. + */ + onValidateVehicleParts: () => void; /** * Callback called when the user clicks on the close button. If this callback is not provided, the close button is not * displayed. @@ -100,6 +118,10 @@ export interface PhotoCaptureHUDProps * The current images taken by the user (ignoring retaken pictures etc.). */ images: Image[]; + /** + * The vehicle type of the inspection. + */ + vehicleType: VehicleType; } /** @@ -114,9 +136,12 @@ export function PhotoCaptureHUD({ sightsTaken, lastPictureTakenUri, mode, + vehicleParts, onSelectSight, onRetakeSight, onAddDamage, + onAddDamagePartsSelected, + onValidateVehicleParts, onCancelAddDamage, onOpenGallery, onRetry, @@ -126,7 +151,7 @@ export function PhotoCaptureHUD({ handle, cameraPreview, images, - enableAddDamage, + addDamage, sightGuidelines, enableSightGuidelines, currentTutorialStep, @@ -134,6 +159,7 @@ export function PhotoCaptureHUD({ onNextTutorialStep, onCloseTutorial, enforceOrientation, + vehicleType, }: PhotoCaptureHUDProps) { const { t } = useTranslation(); const [showCloseModal, setShowCloseModal] = useState(false); @@ -164,35 +190,41 @@ export function PhotoCaptureHUD({ sights={sights} sightsTaken={sightsTaken} mode={mode} + vehicleParts={vehicleParts} onAddDamage={onAddDamage} onCancelAddDamage={onCancelAddDamage} + onAddDamagePartsSelected={onAddDamagePartsSelected} onSelectSight={onSelectSight} onRetakeSight={onRetakeSight} + onValidateVehicleParts={onValidateVehicleParts} isLoading={loading.isLoading} error={loading.error ?? handle.error} previewDimensions={handle.previewDimensions} images={images} - enableAddDamage={enableAddDamage} + addDamage={addDamage} sightGuidelines={sightGuidelines} enableSightGuidelines={enableSightGuidelines} tutorialStep={currentTutorialStep} + vehicleType={vehicleType} /> - setShowCloseModal(true)} - galleryPreview={lastPictureTakenUri ?? undefined} - closeDisabled={!!loading.error || !!handle.error} - galleryDisabled={!!loading.error || !!handle.error} - takePictureDisabled={ - !!loading.error || !!handle.error || handle.isLoading || loading.isLoading - } - showCloseButton={showCloseButton} - showGalleryBadge={retakeCount > 0} - retakeCount={retakeCount} - /> - setShowCloseModal(true)} + galleryPreview={lastPictureTakenUri ?? undefined} + closeDisabled={!!loading.error || !!handle.error} + galleryDisabled={!!loading.error || !!handle.error} + takePictureDisabled={ + !!loading.error || !!handle.error || handle.isLoading || loading.isLoading + } + showCloseButton={showCloseButton} + showGalleryBadge={retakeCount > 0} + retakeCount={retakeCount} + /> + )} + diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/index.ts deleted file mode 100644 index d256f5ba0..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PhotoCaptureHUDButtons, type PhotoCaptureHUDButtonsProps } from './PhotoCaptureHUDButtons'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton/PhotoCaptureHUDCancelButton.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton/PhotoCaptureHUDCancelButton.tsx deleted file mode 100644 index 5395c718e..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton/PhotoCaptureHUDCancelButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Button } from '@monkvision/common-ui-web'; -import { useTranslation } from 'react-i18next'; -import { usePhotoCaptureHUDButtonBackground } from '../hooks'; - -/** - * Props of the PhotoCaptureHUDCancelButton component. - */ -export interface PhotoCaptureHUDCancelButtonProps { - /** - * Callback called when the user clicks on the button. - */ - onCancel?: () => void; -} - -/** - * Component implementing a cancel button displayed in the PhotoCapture Camera HUD. - */ -export function PhotoCaptureHUDCancelButton({ onCancel }: PhotoCaptureHUDCancelButtonProps) { - const { t } = useTranslation(); - const primaryColor = usePhotoCaptureHUDButtonBackground(); - - return ( - - ); -} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton/index.ts deleted file mode 100644 index aa5c1883c..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - PhotoCaptureHUDCancelButton, - type PhotoCaptureHUDCancelButtonProps, -} from './PhotoCaptureHUDCancelButton'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/PhotoCaptureHUDCounter.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/PhotoCaptureHUDCounter.tsx deleted file mode 100644 index 39f0561de..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/PhotoCaptureHUDCounter.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { styles } from './PhotoCaptureHUDCounter.styles'; -import { usePhotoCaptureHUDButtonBackground } from '../hooks'; -import { PhotoCaptureHUDCounterProps, usePhotoCaptureHUDCounterLabel } from './hooks'; - -/** - * Component that implements an indicator of pictures taken during the PhotoCapture process. - */ -export function PhotoCaptureHUDCounter(props: PhotoCaptureHUDCounterProps) { - const label = usePhotoCaptureHUDCounterLabel(props); - const backgroundColor = usePhotoCaptureHUDButtonBackground(); - - return ( -
- {label} -
- ); -} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/index.ts deleted file mode 100644 index 7b2aa8a93..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PhotoCaptureHUDCounter } from './PhotoCaptureHUDCounter'; -export { type PhotoCaptureHUDCounterProps } from './hooks'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx index c0af87444..11c2608d0 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx @@ -1,17 +1,21 @@ -import { PhotoCaptureAppConfig, Image, PixelDimensions, Sight } from '@monkvision/types'; -import { PhotoCaptureMode, TutorialSteps } from '../../hooks'; +import { + PhotoCaptureAppConfig, + Image, + PixelDimensions, + Sight, + VehiclePart, + VehicleType, +} from '@monkvision/types'; +import { TutorialSteps } from '../../hooks'; import { PhotoCaptureHUDElementsSight } from '../PhotoCaptureHUDElementsSight'; -import { PhotoCaptureHUDElementsAddDamage1stShot } from '../PhotoCaptureHUDElementsAddDamage1stShot'; -import { PhotoCaptureHUDElementsAddDamage2ndShot } from '../PhotoCaptureHUDElementsAddDamage2ndShot'; +import { CloseUpShot, ZoomOutShot, PartSelection } from '../../../components'; +import { CaptureMode } from '../../../types'; /** * Props of the PhotoCaptureHUDElements component. */ export interface PhotoCaptureHUDElementsProps - extends Pick< - PhotoCaptureAppConfig, - 'enableSightGuidelines' | 'sightGuidelines' | 'enableAddDamage' - > { + extends Pick { /** * The currently selected sight in the PhotoCapture component : the sight that the user needs to capture. */ @@ -27,15 +31,23 @@ export interface PhotoCaptureHUDElementsProps /** * The current mode of the component. */ - mode: PhotoCaptureMode; + mode: CaptureMode; /** * The current tutorial step in PhotoCapture component. */ tutorialStep: TutorialSteps | null; + /** + * Current vehicle parts selected to take a picture of. + */ + vehicleParts: VehiclePart[]; /** * Callback called when the user presses the Add Damage button. */ onAddDamage: () => void; + /** + * Callback called when the user selects the parts to take a picture of. + */ + onAddDamagePartsSelected?: (parts: VehiclePart[]) => void; /** * Callback called when the user cancels the Add Damage mode. */ @@ -48,6 +60,10 @@ export interface PhotoCaptureHUDElementsProps * Callback called when the user manually selects a sight to retake. */ onRetakeSight: (sight: string) => void; + /** + * Callback called when the user clicks on the "Validate" button of the Add Damage mode. + */ + onValidateVehicleParts: () => void; /** * The effective pixel dimensions of the Camera video stream on the screen. */ @@ -64,6 +80,10 @@ export interface PhotoCaptureHUDElementsProps * The current images taken by the user (ignoring retaken pictures etc.). */ images: Image[]; + /** + * The vehicle type of the inspection. + */ + vehicleType: VehicleType; } /** @@ -73,7 +93,7 @@ export function PhotoCaptureHUDElements(params: PhotoCaptureHUDElementsProps) { if (params.isLoading || !!params.error) { return null; } - if (params.mode === PhotoCaptureMode.SIGHT) { + if (params.mode === CaptureMode.SIGHT) { return ( ); } - if (params.mode === PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT) { - return ; + if (params.mode === CaptureMode.ADD_DAMAGE_1ST_SHOT) { + return ; + } + if ( + [CaptureMode.ADD_DAMAGE_2ND_SHOT, CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT].includes(params.mode) + ) { + return ( + + ); } return ( - ); } diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/PhotoCaptureHUDElementsAddDamage1stShot.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/PhotoCaptureHUDElementsAddDamage1stShot.tsx deleted file mode 100644 index 448c1386f..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/PhotoCaptureHUDElementsAddDamage1stShot.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Button, DynamicSVG } from '@monkvision/common-ui-web'; -import { usePhotoCaptureHUDButtonBackground } from '../hooks'; -import { styles } from './PhotoCaptureHUDElementsAddDamage1stShot.styles'; -import { PhotoCaptureHUDCounter } from '../PhotoCaptureHUDCounter'; -import { PhotoCaptureMode } from '../../hooks'; -import { PhotoCaptureHUDCancelButton } from '../PhotoCaptureHUDCancelButton'; -import { crosshairSvg } from '../../../assets'; -import { usePhotoCaptureHUDElementsAddDamage1stShotStyles } from './hooks'; - -/** - * Props of the PhotoCaptureHUDElementsAddDamage1stShot component. - */ -export interface PhotoCaptureHUDElementsAddDamage1stShotProps { - /** - * Callback called when the user cancels the Add Damage mode. - */ - onCancel?: () => void; -} - -/** - * Component implementing an HUD displayed on top of the Camera preview during the PhotoCapture process when the current - * mode is ADD_DAMAGE_1ST_SHOT. - */ -export function PhotoCaptureHUDElementsAddDamage1stShot({ - onCancel, -}: PhotoCaptureHUDElementsAddDamage1stShotProps) { - const [showInfoPopup, setShowInfoPopup] = useState(true); - const { t } = useTranslation(); - const primaryColor = usePhotoCaptureHUDButtonBackground(); - const style = usePhotoCaptureHUDElementsAddDamage1stShotStyles(); - - return ( -
- -
- - -
- {showInfoPopup && ( - - )} -
- ); -} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/index.ts deleted file mode 100644 index 359fef407..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - PhotoCaptureHUDElementsAddDamage1stShot, - type PhotoCaptureHUDElementsAddDamage1stShotProps, -} from './PhotoCaptureHUDElementsAddDamage1stShot'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/PhotoCaptureHUDElementsAddDamage2ndShot.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/PhotoCaptureHUDElementsAddDamage2ndShot.tsx deleted file mode 100644 index 3b1d127ed..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/PhotoCaptureHUDElementsAddDamage2ndShot.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { PixelDimensions } from '@monkvision/types'; -import { useTranslation } from 'react-i18next'; -import { isMobileDevice } from '@monkvision/common'; -import { PhotoCaptureMode } from '../../hooks'; -import { styles } from './PhotoCaptureHUDElementsAddDamage2ndShot.styles'; -import { PhotoCaptureHUDCounter } from '../PhotoCaptureHUDCounter'; -import { PhotoCaptureHUDCancelButton } from '../PhotoCaptureHUDCancelButton'; -import { usePhotoCaptureHUDElementsAddDamage2ndShotStyle } from './hooks'; - -/** - * Props of the PhotoCaptureHUDElementsAddDamage2ndShot component. - */ -export interface PhotoCaptureHUDElementsAddDamage2ndShotProps { - /** - * Callback called when the user cancels the Add Damage mode. - */ - onCancel?: () => void; - /** - * The dimensions of the Camera video stream. - */ - streamDimensions?: PixelDimensions | null; -} - -function getAspectRatio(streamDimensions?: PixelDimensions | null) { - if (isMobileDevice() && streamDimensions) { - return `${streamDimensions?.width}/${streamDimensions?.height}`; - } - return '16/9'; -} - -/** - * Component implementing an HUD displayed on top of the Camera preview during the PhotoCapture process when the current - * mode is ADD_DAMAGE_2ND_SHOT. - */ -export function PhotoCaptureHUDElementsAddDamage2ndShot({ - onCancel, - streamDimensions, -}: PhotoCaptureHUDElementsAddDamage2ndShotProps) { - const { t } = useTranslation(); - const style = usePhotoCaptureHUDElementsAddDamage2ndShotStyle(); - - const aspectRatio = getAspectRatio(streamDimensions); - - return ( -
-
-
-
-
- - -
-
{t('photo.hud.addDamage.infoCloseup')}
-
- ); -} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/index.ts deleted file mode 100644 index 84762a2e9..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - PhotoCaptureHUDElementsAddDamage2ndShot, - type PhotoCaptureHUDElementsAddDamage2ndShotProps, -} from './PhotoCaptureHUDElementsAddDamage2ndShot'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx index 44e5dbf65..ad226fce8 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx @@ -1,6 +1,7 @@ import { Button } from '@monkvision/common-ui-web'; import { useTranslation } from 'react-i18next'; -import { usePhotoCaptureHUDButtonBackground } from '../../hooks'; +import { AddDamage } from '@monkvision/types'; +import { useColorBackground } from '../../../../hooks'; /** * Props of the AddDamageButton component. @@ -12,31 +13,25 @@ export interface AddDamageButtonProps { onAddDamage?: () => void; /** * Boolean indicating whether the Add Damage feature is enabled. If disabled, the `Add Damage` button will be hidden. - * - * @default true */ - enableAddDamage?: boolean; + addDamage?: AddDamage; } /** * Custom button displayed in the PhotoCapture Camera HUD that allows user to enter add damage mode. */ -export function AddDamageButton({ onAddDamage, enableAddDamage }: AddDamageButtonProps) { +export function AddDamageButton({ onAddDamage, addDamage }: AddDamageButtonProps) { const { t } = useTranslation(); - const primaryColor = usePhotoCaptureHUDButtonBackground(); + const primaryColor = useColorBackground(); - return ( - <> - {enableAddDamage && ( - - )} - - ); + return [AddDamage.TWO_SHOT, AddDamage.PART_SELECT].includes(addDamage as AddDamage) ? ( + + ) : null; } diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx index eca17bf92..fcfc52897 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx @@ -3,9 +3,10 @@ import { SightSlider } from './SightSlider'; import { styles } from './PhotoCaptureHUDElementsSight.styles'; import { AddDamageButton } from './AddDamageButton'; import { PhotoCaptureHUDElementsSightProps, usePhotoCaptureHUDSightPreviewStyle } from './hooks'; -import { PhotoCaptureHUDCounter } from '../PhotoCaptureHUDCounter'; -import { PhotoCaptureMode, TutorialSteps } from '../../hooks'; +import { TutorialSteps } from '../../hooks'; import { SightGuideline } from './SightGuideline'; +import { Counter } from '../../../components'; +import { CaptureMode } from '../../../types'; /** * Component implementing an HUD displayed on top of the Camera preview during the PhotoCapture process when the current @@ -20,7 +21,7 @@ export function PhotoCaptureHUDElementsSight({ sightsTaken, previewDimensions, images, - enableAddDamage, + addDamage, sightGuidelines, enableSightGuidelines, tutorialStep, @@ -39,13 +40,13 @@ export function PhotoCaptureHUDElementsSight({ sightId={selectedSight.id} sightGuidelines={sightGuidelines} enableSightGuidelines={enableSightGuidelines} - enableAddDamage={enableAddDamage} + addDamage={addDamage} /> - +
- diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts index b89e33474..d716c000c 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts @@ -1,5 +1,5 @@ import { Styles } from '@monkvision/types'; -import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../../PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles'; +import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../../../../components/HUDButtons/HUDButtons.styles'; export const styles: Styles = { container: { diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx index 8f8c3306d..0772c716d 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx @@ -1,19 +1,16 @@ import { useEffect, useState } from 'react'; import { Button } from '@monkvision/common-ui-web'; -import { PhotoCaptureAppConfig } from '@monkvision/types'; +import { AddDamage, PhotoCaptureAppConfig } from '@monkvision/types'; import { useTranslation } from 'react-i18next'; import { getLanguage } from '@monkvision/common'; -import { usePhotoCaptureHUDButtonBackground } from '../../hooks'; import { styles } from './SightGuideline.styles'; +import { useColorBackground } from '../../../../hooks'; /** * Props of the SightGuideline component. */ export interface SightGuidelineProps - extends Pick< - PhotoCaptureAppConfig, - 'enableAddDamage' | 'sightGuidelines' | 'enableSightGuidelines' - > { + extends Pick { /** * The id of the sight. */ @@ -33,14 +30,14 @@ export function SightGuideline({ sightId, sightGuidelines, enableSightGuidelines, - enableAddDamage, + addDamage, enableDefaultMessage = false, }: SightGuidelineProps) { const [showGuideline, setShowGuideline] = useState(true); - const primaryColor = usePhotoCaptureHUDButtonBackground(); + const primaryColor = useColorBackground(); const { i18n, t } = useTranslation(); - const style = enableAddDamage ? styles['container'] : styles['containerWide']; + const style = addDamage === AddDamage.DISABLED ? styles['containerWide'] : styles['container']; const guidelineFound = sightGuidelines?.find((value) => value.sightIds.includes(sightId)); diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSlider.styles.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSlider.styles.ts index 63e05012f..732a0c72c 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSlider.styles.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSlider.styles.ts @@ -1,5 +1,5 @@ import { Styles } from '@monkvision/types'; -import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../../PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles'; +import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../../../../components/HUDButtons/HUDButtons.styles'; export const styles: Styles = { container: { diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts index 13c214b6a..cf4960f9a 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts @@ -8,10 +8,7 @@ import { TutorialSteps } from '../../hooks'; * Props of the PhotoCaptureHUDElementsSight component. */ export interface PhotoCaptureHUDElementsSightProps - extends Pick< - PhotoCaptureAppConfig, - 'enableSightGuidelines' | 'sightGuidelines' | 'enableAddDamage' - > { + extends Pick { /** * The list of sights provided to the PhotoCapture component. */ diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/index.ts deleted file mode 100644 index 6952ada6b..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PhotoCaptureHUDOverlay } from './PhotoCaptureHUDOverlay'; -export { type PhotoCaptureHUDOverlayProps } from './hooks'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.styles.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.styles.ts index 078f12dbf..d616a3a7b 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.styles.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.styles.ts @@ -1,5 +1,5 @@ import { Styles } from '@monkvision/types'; -import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles'; +import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../../../components/HUDButtons/HUDButtons.styles'; export const styles: Styles = { backdropContainer: { diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx index 783917224..d10d97d23 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx @@ -4,16 +4,16 @@ import { Button } from '@monkvision/common-ui-web'; import { PhotoCaptureAppConfig } from '@monkvision/types'; import { styles } from './PhotoCaptureHUDTutorial.styles'; import { TutorialSteps } from '../../hooks'; -import { usePhotoCaptureHUDButtonBackground } from '../hooks'; import { SightGuideline } from '../PhotoCaptureHUDElementsSight'; import { ArrowIcon } from './ArrowIcon'; import { DisplayText } from './DisplayText'; +import { useColorBackground } from '../../../hooks'; /** * Props of the PhotoCaptureHUDTutorial component. */ export interface PhotoCaptureHUDTutorialProps - extends Pick { + extends Pick { /** * The id of the sight. */ @@ -46,9 +46,10 @@ export function PhotoCaptureHUDTutorial({ sightId, onNextTutorialStep, onCloseTutorial, + addDamage, }: PhotoCaptureHUDTutorialProps) { const { t } = useTranslation(); - const primaryColor = usePhotoCaptureHUDButtonBackground(); + const primaryColor = useColorBackground(); return currentTutorialStep ? (
@@ -60,7 +61,7 @@ export function PhotoCaptureHUDTutorial({ sightId={sightId} sightGuidelines={sightGuidelines} enableSightGuidelines={currentTutorialStep === TutorialSteps.GUIDELINE} - enableAddDamage={true} + addDamage={addDamage} enableDefaultMessage={true} /> + ); +} diff --git a/packages/inspection-capture-web/src/components/CancelButton/index.ts b/packages/inspection-capture-web/src/components/CancelButton/index.ts new file mode 100644 index 000000000..fbc0346fe --- /dev/null +++ b/packages/inspection-capture-web/src/components/CancelButton/index.ts @@ -0,0 +1 @@ +export { CancelButton, type CancelButtonProps } from './CancelButton'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/PhotoCaptureHUDElementsAddDamage2ndShot.styles.ts b/packages/inspection-capture-web/src/components/CloseUpShot/CloseUpShot.styles.ts similarity index 93% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/PhotoCaptureHUDElementsAddDamage2ndShot.styles.ts rename to packages/inspection-capture-web/src/components/CloseUpShot/CloseUpShot.styles.ts index 4f69c890a..7788d0d96 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/PhotoCaptureHUDElementsAddDamage2ndShot.styles.ts +++ b/packages/inspection-capture-web/src/components/CloseUpShot/CloseUpShot.styles.ts @@ -1,5 +1,5 @@ import { Styles } from '@monkvision/types'; -import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles'; +import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../HUDButtons/HUDButtons.styles'; export const styles: Styles = { container: { @@ -32,6 +32,7 @@ export const styles: Styles = { frameContainer: { position: 'absolute', width: '100%', + maxHeight: '100%', }, frame: { position: 'absolute', diff --git a/packages/inspection-capture-web/src/components/CloseUpShot/CloseUpShot.tsx b/packages/inspection-capture-web/src/components/CloseUpShot/CloseUpShot.tsx new file mode 100644 index 000000000..4c4dc4998 --- /dev/null +++ b/packages/inspection-capture-web/src/components/CloseUpShot/CloseUpShot.tsx @@ -0,0 +1,54 @@ +import { PixelDimensions } from '@monkvision/types'; +import { useTranslation } from 'react-i18next'; +import { getAspectRatio } from '@monkvision/common'; +import { styles } from './CloseUpShot.styles'; +import { useCloseUpShotStyle } from './hooks'; +import { Counter } from '../Counter'; +import { CancelButton } from '../CancelButton'; +import { CaptureMode } from '../../types'; + +/** + * Props of the CloseUpShot component. + */ +export interface CloseUpShotProps { + /** + * Callback called when the user cancels the Add Damage mode. + */ + onCancel?: () => void; + /** + * Boolean indicating whether the counter should be displayed. + * + * @default true + */ + showCounter?: boolean; + /** + * The dimensions of the Camera video stream. + */ + streamDimensions?: PixelDimensions | null; +} + +/** + * Component implementing an HUD displayed on top of the Camera preview during the PhotoCapture process when the current + * mode is ADD_DAMAGE_2ND_SHOT | ADD_DAMAGE_PART_SELECT_SHOT. + */ +export function CloseUpShot({ onCancel, showCounter = true, streamDimensions }: CloseUpShotProps) { + const { t } = useTranslation(); + const style = useCloseUpShotStyle(); + + const aspectRatio = getAspectRatio(streamDimensions); + + return ( +
+
+
+
+
+ {showCounter && } + +
+
+ {t('photo.hud.addDamage.infoCloseup')} +
+
+ ); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/hooks.ts b/packages/inspection-capture-web/src/components/CloseUpShot/hooks.ts similarity index 67% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/hooks.ts rename to packages/inspection-capture-web/src/components/CloseUpShot/hooks.ts index eb778a717..857eece25 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot/hooks.ts +++ b/packages/inspection-capture-web/src/components/CloseUpShot/hooks.ts @@ -1,14 +1,14 @@ import { CSSProperties } from 'react'; import { useResponsiveStyle } from '@monkvision/common'; -import { styles } from './PhotoCaptureHUDElementsAddDamage2ndShot.styles'; +import { styles } from './CloseUpShot.styles'; -export interface PhotoCaptureHUDElementsAddDamage2ndShotStyle { +export interface CloseUpShotStyle { top: CSSProperties; infoCloseup: CSSProperties; frame: CSSProperties; } -export function usePhotoCaptureHUDElementsAddDamage2ndShotStyle(): PhotoCaptureHUDElementsAddDamage2ndShotStyle { +export function useCloseUpShotStyle(): CloseUpShotStyle { const { responsive } = useResponsiveStyle(); return { diff --git a/packages/inspection-capture-web/src/components/CloseUpShot/index.ts b/packages/inspection-capture-web/src/components/CloseUpShot/index.ts new file mode 100644 index 000000000..00369aecf --- /dev/null +++ b/packages/inspection-capture-web/src/components/CloseUpShot/index.ts @@ -0,0 +1,4 @@ +export { + CloseUpShot, + type CloseUpShotProps as PhotoCaptureHUDElementsAddDamage2ndShotProps, +} from './CloseUpShot'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/PhotoCaptureHUDCounter.styles.ts b/packages/inspection-capture-web/src/components/Counter/Counter.styles.ts similarity index 100% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/PhotoCaptureHUDCounter.styles.ts rename to packages/inspection-capture-web/src/components/Counter/Counter.styles.ts diff --git a/packages/inspection-capture-web/src/components/Counter/Counter.tsx b/packages/inspection-capture-web/src/components/Counter/Counter.tsx new file mode 100644 index 000000000..0384f793b --- /dev/null +++ b/packages/inspection-capture-web/src/components/Counter/Counter.tsx @@ -0,0 +1,17 @@ +import { styles } from './Counter.styles'; +import { useColorBackground } from '../../hooks'; +import { CounterProps, useCounterLabel } from './hooks'; + +/** + * Component that implements an indicator of pictures taken during the capture process. + */ +export function Counter(props: CounterProps) { + const label = useCounterLabel(props); + const backgroundColor = useColorBackground(); + + return ( +
+ {label} +
+ ); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/hooks.ts b/packages/inspection-capture-web/src/components/Counter/hooks.ts similarity index 57% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/hooks.ts rename to packages/inspection-capture-web/src/components/Counter/hooks.ts index 942f186cf..6c5ea1e9f 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter/hooks.ts +++ b/packages/inspection-capture-web/src/components/Counter/hooks.ts @@ -1,15 +1,15 @@ import { useTranslation } from 'react-i18next'; -import { PhotoCaptureMode } from '../../hooks'; +import { CaptureMode } from '../../types'; /** - * Props of the PhotoCaptureHUDCounter component. + * Props of the Counter component. */ -export type PhotoCaptureHUDCounterProps = +export type CounterProps = | { /** * The current mode of the PhotoCapture component. */ - mode: PhotoCaptureMode.SIGHT; + mode: CaptureMode.SIGHT; /** * The total number of sights given to the PhotoCapture component. */ @@ -23,16 +23,20 @@ export type PhotoCaptureHUDCounterProps = /** * The current mode of the PhotoCapture component. */ - mode: PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT | PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT; + mode: + | CaptureMode.ADD_DAMAGE_1ST_SHOT + | CaptureMode.ADD_DAMAGE_2ND_SHOT + | CaptureMode.ADD_DAMAGE_PART_SELECT + | CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT; }; -export function usePhotoCaptureHUDCounterLabel(props: PhotoCaptureHUDCounterProps): string { +export function useCounterLabel(props: CounterProps): string { const { t } = useTranslation(); - if (props.mode === PhotoCaptureMode.SIGHT) { + if (props.mode === CaptureMode.SIGHT) { return `${props.sightsTaken} / ${props.totalSights}`; } - if (props.mode === PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT) { + if (props.mode === CaptureMode.ADD_DAMAGE_1ST_SHOT) { return t('photo.hud.addDamage.damagedPartCounter'); } return t('photo.hud.addDamage.closeupPictureCounter'); diff --git a/packages/inspection-capture-web/src/components/Counter/index.ts b/packages/inspection-capture-web/src/components/Counter/index.ts new file mode 100644 index 000000000..908dc74ca --- /dev/null +++ b/packages/inspection-capture-web/src/components/Counter/index.ts @@ -0,0 +1,2 @@ +export { Counter } from './Counter'; +export { type CounterProps } from './hooks'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles.ts b/packages/inspection-capture-web/src/components/HUDButtons/HUDButtons.styles.ts similarity index 100% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles.ts rename to packages/inspection-capture-web/src/components/HUDButtons/HUDButtons.styles.ts diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx b/packages/inspection-capture-web/src/components/HUDButtons/HUDButtons.tsx similarity index 90% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx rename to packages/inspection-capture-web/src/components/HUDButtons/HUDButtons.tsx index 23788e848..99da51790 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx +++ b/packages/inspection-capture-web/src/components/HUDButtons/HUDButtons.tsx @@ -1,11 +1,11 @@ import { Icon, TakePictureButton } from '@monkvision/common-ui-web'; import { useInteractiveStatus } from '@monkvision/common'; -import { useCaptureHUDButtonsStyles } from './hooks'; +import { useHUDButtonsStyles } from './hooks'; /** - * Props of the PhotoCaptureHUDButtons component. + * Props of the HUDButtons component. */ -export interface PhotoCaptureHUDButtonsProps { +export interface HUDButtonsProps { /** * URI of the picture displayed in the gallery button icon. Usually, this is the last picture taken by the user. If no * picture is provided, a gallery icon will be displayed instead. @@ -63,12 +63,12 @@ export interface PhotoCaptureHUDButtonsProps { } /** - * Components implementing the main buttons of the PhotoCapture Camera HUD. This component implements 3 buttons : + * Components implementing the main buttons of the capture Camera HUD. This component implements 3 buttons : * - A take picture button * - A gallery button * - A close button (only displayed if the `onClose` callback is defined) */ -export function PhotoCaptureHUDButtons({ +export function HUDButtons({ galleryPreview, onTakePicture, onOpenGallery, @@ -79,7 +79,7 @@ export function PhotoCaptureHUDButtons({ showCloseButton = false, showGalleryBadge = false, retakeCount = 0, -}: PhotoCaptureHUDButtonsProps) { +}: HUDButtonsProps) { const { status: galleryStatus, eventHandlers: galleryEventHandlers } = useInteractiveStatus({ disabled: galleryDisabled, }); @@ -87,7 +87,7 @@ export function PhotoCaptureHUDButtons({ disabled: closeDisabled, }); const { containerStyle, gallery, galleryBadgeStyle, close, backgroundCoverStyle } = - useCaptureHUDButtonsStyles({ + useHUDButtonsStyles({ galleryStatus, closeStatus, closeBtnAvailable: !!onClose, diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/hooks.ts b/packages/inspection-capture-web/src/components/HUDButtons/hooks.ts similarity index 91% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/hooks.ts rename to packages/inspection-capture-web/src/components/HUDButtons/hooks.ts index 75b04e54f..e2edf247e 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/hooks.ts +++ b/packages/inspection-capture-web/src/components/HUDButtons/hooks.ts @@ -10,9 +10,9 @@ import { captureButtonBackgroundColors, captureButtonForegroundColors, styles, -} from './PhotoCaptureHUDButtons.styles'; +} from './HUDButtons.styles'; -interface PhotoCaptureHUDButtonsStylesParams { +interface HUDButtonsStylesParams { galleryStatus: InteractiveStatus; closeStatus: InteractiveStatus; closeBtnAvailable: boolean; @@ -21,7 +21,7 @@ interface PhotoCaptureHUDButtonsStylesParams { showGalleryBadge?: boolean; } -interface PhotoCaptureHUDButtonsStyles { +interface HUDButtonsStyles { containerStyle: CSSProperties; gallery: { style: CSSProperties; @@ -37,9 +37,7 @@ interface PhotoCaptureHUDButtonsStyles { const ANIMATION_DELAY_MS = 50; -export function useCaptureHUDButtonsStyles( - params: PhotoCaptureHUDButtonsStylesParams, -): PhotoCaptureHUDButtonsStyles { +export function useHUDButtonsStyles(params: HUDButtonsStylesParams): HUDButtonsStyles { const [backgroundAnimationStart, setBackgroundAnimationStart] = useState(false); const { responsive } = useResponsiveStyle(); const { palette } = useMonkTheme(); diff --git a/packages/inspection-capture-web/src/components/HUDButtons/index.ts b/packages/inspection-capture-web/src/components/HUDButtons/index.ts new file mode 100644 index 000000000..cc828c459 --- /dev/null +++ b/packages/inspection-capture-web/src/components/HUDButtons/index.ts @@ -0,0 +1 @@ +export { HUDButtons, type HUDButtonsProps } from './HUDButtons'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/PhotoCaptureHUDOverlay.styles.ts b/packages/inspection-capture-web/src/components/HUDOverlay/HUDOverlay.styles.ts similarity index 100% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/PhotoCaptureHUDOverlay.styles.ts rename to packages/inspection-capture-web/src/components/HUDOverlay/HUDOverlay.styles.ts diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/PhotoCaptureHUDOverlay.tsx b/packages/inspection-capture-web/src/components/HUDOverlay/HUDOverlay.tsx similarity index 76% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/PhotoCaptureHUDOverlay.tsx rename to packages/inspection-capture-web/src/components/HUDOverlay/HUDOverlay.tsx index 53485a1d6..54805e80b 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/PhotoCaptureHUDOverlay.tsx +++ b/packages/inspection-capture-web/src/components/HUDOverlay/HUDOverlay.tsx @@ -1,23 +1,23 @@ import { useTranslation } from 'react-i18next'; import { Button, Spinner } from '@monkvision/common-ui-web'; import { useResponsiveStyle } from '@monkvision/common'; -import { styles } from './PhotoCaptureHUDOverlay.styles'; -import { PhotoCaptureHUDOverlayProps, usePhotoCaptureErrorLabel, useRetry } from './hooks'; +import { styles } from './HUDOverlay.styles'; +import { HUDOverlayProps, useErrorLabel, useRetry } from './hooks'; /** - * Component that displays an overlay on top of the PhotoCapture component that is used to display elements such as - * error messages, spinning loaders etc. + * Component that displays an overlay on top of the PhotoCapture/DamageDisclosure component that is used + * to display elements such as error messages, spinning loaders etc. */ -export function PhotoCaptureHUDOverlay({ +export function HUDOverlay({ isCaptureLoading, captureError, handle, onRetry, inspectionId, -}: PhotoCaptureHUDOverlayProps) { +}: HUDOverlayProps) { const { t } = useTranslation(); const { responsive } = useResponsiveStyle(); - const error = usePhotoCaptureErrorLabel(captureError, handle, inspectionId); + const error = useErrorLabel(captureError, handle, inspectionId); const handleRetry = useRetry({ captureError, handle, onRetry }); if (!isCaptureLoading && !error) { diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/hooks.ts b/packages/inspection-capture-web/src/components/HUDOverlay/hooks.ts similarity index 91% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/hooks.ts rename to packages/inspection-capture-web/src/components/HUDOverlay/hooks.ts index cc6f8675b..f2a408868 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay/hooks.ts +++ b/packages/inspection-capture-web/src/components/HUDOverlay/hooks.ts @@ -5,9 +5,9 @@ import { MonkNetworkError } from '@monkvision/network'; import { PhotoCaptureErrorName } from '../../errors'; /** - * Props of the PhotoCaptureHUDOverlay component. + * Props of the HUDOverlay component. */ -export interface PhotoCaptureHUDOverlayProps { +export interface HUDOverlayProps { /** * Boolean indicating if the global loading state of the PhotoCapture component is loading or not. */ @@ -30,7 +30,7 @@ export interface PhotoCaptureHUDOverlayProps { inspectionId: string; } -export function usePhotoCaptureErrorLabel( +export function useErrorLabel( captureError: unknown | null, handle: CameraHandle, inspectionId: string, @@ -71,7 +71,7 @@ export function useRetry({ captureError, handle, onRetry, -}: Pick): (() => void) | null { +}: Pick): (() => void) | null { if (handle.error) { return handle.retry; } diff --git a/packages/inspection-capture-web/src/components/HUDOverlay/index.ts b/packages/inspection-capture-web/src/components/HUDOverlay/index.ts new file mode 100644 index 000000000..fbc390230 --- /dev/null +++ b/packages/inspection-capture-web/src/components/HUDOverlay/index.ts @@ -0,0 +1,2 @@ +export { HUDOverlay } from './HUDOverlay'; +export { type HUDOverlayProps } from './hooks'; diff --git a/packages/inspection-capture-web/src/components/PartSelection/PartSelection.styles.ts b/packages/inspection-capture-web/src/components/PartSelection/PartSelection.styles.ts new file mode 100644 index 000000000..531e3f5fa --- /dev/null +++ b/packages/inspection-capture-web/src/components/PartSelection/PartSelection.styles.ts @@ -0,0 +1,53 @@ +import { Styles } from '@monkvision/types'; + +export const styles: Styles = { + container: { + position: 'fixed', + display: 'flex', + flexDirection: 'column', + justifyItems: 'center', + alignItems: 'center', + inset: 0, + }, + vehicleSelect: { + alignSelf: 'stretch', + justifySelf: 'stretch', + position: 'fixed', + height: '85%', + width: '75%', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + zIndex: 9, + }, + labelsContainer: { + position: 'fixed', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'center', + top: '10px', + bottom: '10px', + width: '90%', + }, + partsLabel: { + display: 'flex', + justifyContent: 'center', + }, + tutoLabel: { + display: 'flex', + justifyContent: 'center', + }, + closeBtn: { + position: 'fixed', + top: '5px', + left: '5px', + }, + validateBtn: { + position: 'fixed', + top: '50%', + right: '10px', + transform: 'translate(-50%, -50%)', + zIndex: 10, + }, +}; diff --git a/packages/inspection-capture-web/src/components/PartSelection/PartSelection.tsx b/packages/inspection-capture-web/src/components/PartSelection/PartSelection.tsx new file mode 100644 index 000000000..4907d0ee5 --- /dev/null +++ b/packages/inspection-capture-web/src/components/PartSelection/PartSelection.tsx @@ -0,0 +1,110 @@ +import { useMonkTheme, vehiclePartLabels, getLanguage } from '@monkvision/common'; +import { VehiclePartSelection, Button } from '@monkvision/common-ui-web'; +import { VehiclePart, VehicleType } from '@monkvision/types'; +import { useTranslation } from 'react-i18next'; +import { styles } from './PartSelection.styles'; +import { useColorBackground } from '../../hooks'; + +/** + * Props of PartSelection component. + */ +export interface PartSelectionProps { + /** + * Vehicle type of the wireframe to display. + */ + vehicleType: VehicleType; + /** + * Current vehicle parts selected to take a picture of. + */ + vehicleParts: VehiclePart[]; + /** + * Boolean indicating if the vehicle part selector is currently displayed. + */ + disabled?: boolean; + /** + * Callback called when the user cancels the Add Damage mode. + */ + onCancel?: () => void; + /** + * Callback called when the user selects the parts to take a picture of. + */ + onAddDamagePartsSelected?: (parts: VehiclePart[]) => void; + /** + * Callback called when the user clicks on the "Validate" button of the Add Damage mode. + */ + onValidateVehicleParts?: () => void; + /** + * The maximum number of parts that can be selected. + */ + maxSelectableParts?: number; +} + +/** + * Component that displays a vehicle wireframe on top of the Camera Preview that is used + * to select the parts of the vehicle that the user wants to take a picture of. + */ +export function PartSelection({ + vehicleType, + vehicleParts, + disabled = false, + onCancel = () => {}, + onAddDamagePartsSelected = () => {}, + onValidateVehicleParts = () => {}, + maxSelectableParts = 1, +}: PartSelectionProps) { + const { palette } = useMonkTheme(); + const { i18n, t } = useTranslation(); + const backgroundColor = useColorBackground(0.9); + const buttonColor = useColorBackground(); + + if (!vehicleType) { + throw new Error('Vehicle type state is not found from useMonkAppState'); + } + const instruction = + maxSelectableParts === 1 ? 'photo.hud.addDamage.selectPart' : 'photo.hud.addDamage.selectParts'; + + return disabled ? null : ( +
+
+ +
+ +
+ {vehicleParts.length > 0 && ( + + + + )} +
+
+ ); +} diff --git a/packages/inspection-capture-web/src/components/PartSelection/index.ts b/packages/inspection-capture-web/src/components/PartSelection/index.ts new file mode 100644 index 000000000..f24dd5571 --- /dev/null +++ b/packages/inspection-capture-web/src/components/PartSelection/index.ts @@ -0,0 +1,2 @@ +export { PartSelection } from './PartSelection'; +export { type PartSelectionProps } from './PartSelection'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/PhotoCaptureHUDElementsAddDamage1stShot.styles.ts b/packages/inspection-capture-web/src/components/ZoomOutShot/ZoomOutShot.styles.ts similarity index 88% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/PhotoCaptureHUDElementsAddDamage1stShot.styles.ts rename to packages/inspection-capture-web/src/components/ZoomOutShot/ZoomOutShot.styles.ts index ecf1b2d66..651e853ba 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/PhotoCaptureHUDElementsAddDamage1stShot.styles.ts +++ b/packages/inspection-capture-web/src/components/ZoomOutShot/ZoomOutShot.styles.ts @@ -1,5 +1,5 @@ import { Styles } from '@monkvision/types'; -import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles'; +import { PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH } from '../HUDButtons/HUDButtons.styles'; export const styles: Styles = { container: { diff --git a/packages/inspection-capture-web/src/components/ZoomOutShot/ZoomOutShot.tsx b/packages/inspection-capture-web/src/components/ZoomOutShot/ZoomOutShot.tsx new file mode 100644 index 000000000..2f4fee1f7 --- /dev/null +++ b/packages/inspection-capture-web/src/components/ZoomOutShot/ZoomOutShot.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, DynamicSVG } from '@monkvision/common-ui-web'; +import { styles } from './ZoomOutShot.styles'; +import { useColorBackground } from '../../hooks'; +import { CaptureMode } from '../../types'; +import { crosshairSvg } from '../../assets'; +import { useZoomOutShotStyles } from './hooks'; +import { Counter } from '../Counter'; +import { CancelButton } from '../CancelButton'; + +/** + * Props of the ZoomOutShot component. + */ +export interface ZoomOutShotProps { + /** + * Callback called when the user cancels the Add Damage mode. + */ + onCancel?: () => void; +} + +/** + * Component implementing an HUD displayed on top of the Camera preview during the PhotoCapture process when the current + * mode is ADD_DAMAGE_1ST_SHOT. + */ +export function ZoomOutShot({ onCancel }: ZoomOutShotProps) { + const [showInfoPopup, setShowInfoPopup] = useState(true); + const { t } = useTranslation(); + const primaryColor = useColorBackground(); + const style = useZoomOutShotStyles(); + + return ( +
+ +
+ + +
+ {showInfoPopup && ( + + )} +
+ ); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/hooks.ts b/packages/inspection-capture-web/src/components/ZoomOutShot/hooks.ts similarity index 61% rename from packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/hooks.ts rename to packages/inspection-capture-web/src/components/ZoomOutShot/hooks.ts index deaa2eebb..12d171eaf 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot/hooks.ts +++ b/packages/inspection-capture-web/src/components/ZoomOutShot/hooks.ts @@ -1,13 +1,13 @@ import { CSSProperties } from 'react'; import { useResponsiveStyle } from '@monkvision/common'; -import { styles } from './PhotoCaptureHUDElementsAddDamage1stShot.styles'; +import { styles } from './ZoomOutShot.styles'; -export interface PhotoCaptureHUDElementsAddDamage1stShotStyle { +export interface ZoomOutShotStyle { top: CSSProperties; infoBtn: CSSProperties; } -export function usePhotoCaptureHUDElementsAddDamage1stShotStyles(): PhotoCaptureHUDElementsAddDamage1stShotStyle { +export function useZoomOutShotStyles(): ZoomOutShotStyle { const { responsive } = useResponsiveStyle(); return { diff --git a/packages/inspection-capture-web/src/components/ZoomOutShot/index.ts b/packages/inspection-capture-web/src/components/ZoomOutShot/index.ts new file mode 100644 index 000000000..0b495235e --- /dev/null +++ b/packages/inspection-capture-web/src/components/ZoomOutShot/index.ts @@ -0,0 +1 @@ +export { ZoomOutShot, type ZoomOutShotProps } from './ZoomOutShot'; diff --git a/packages/inspection-capture-web/src/components/index.ts b/packages/inspection-capture-web/src/components/index.ts index 4c205359f..dd4524732 100644 --- a/packages/inspection-capture-web/src/components/index.ts +++ b/packages/inspection-capture-web/src/components/index.ts @@ -1 +1,8 @@ export * from './OrientationEnforcer'; +export * from './CancelButton'; +export * from './CloseUpShot'; +export * from './HUDButtons'; +export * from './PartSelection'; +export * from './Counter'; +export * from './ZoomOutShot'; +export * from './HUDOverlay'; diff --git a/packages/inspection-capture-web/src/errors.ts b/packages/inspection-capture-web/src/errors.ts new file mode 100644 index 000000000..ddd92ec89 --- /dev/null +++ b/packages/inspection-capture-web/src/errors.ts @@ -0,0 +1,3 @@ +export enum PhotoCaptureErrorName { + MISSING_TASK_IN_INSPECTION = 'PhotoCaptureMissingTaskInInspection', +} diff --git a/packages/inspection-capture-web/src/hooks/index.ts b/packages/inspection-capture-web/src/hooks/index.ts index 9be9ee168..178a39fbe 100644 --- a/packages/inspection-capture-web/src/hooks/index.ts +++ b/packages/inspection-capture-web/src/hooks/index.ts @@ -1,2 +1,10 @@ export * from './useStartTasksOnComplete'; export * from './useEnforceOrientation'; +export * from './useColorBackground'; +export * from './useAddDamageMode'; +export * from './useUploadQueue'; +export * from './usePictureTaken'; +export * from './usePhotoCaptureImages'; +export * from './useBadConnectionWarning'; +export * from './useAdaptiveCameraConfig'; +export * from './useTracking'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/useAdaptiveCameraConfig.ts b/packages/inspection-capture-web/src/hooks/useAdaptiveCameraConfig.ts similarity index 100% rename from packages/inspection-capture-web/src/PhotoCapture/hooks/useAdaptiveCameraConfig.ts rename to packages/inspection-capture-web/src/hooks/useAdaptiveCameraConfig.ts diff --git a/packages/inspection-capture-web/src/hooks/useAddDamageMode.ts b/packages/inspection-capture-web/src/hooks/useAddDamageMode.ts new file mode 100644 index 000000000..e876f104a --- /dev/null +++ b/packages/inspection-capture-web/src/hooks/useAddDamageMode.ts @@ -0,0 +1,140 @@ +import { useCallback, useState } from 'react'; +import { useObjectMemo } from '@monkvision/common'; +import { useAnalytics } from '@monkvision/analytics'; +import { AddDamage, PhotoCaptureAppConfig, VehiclePart } from '@monkvision/types'; +import { CaptureMode, CaptureScreen } from '../types'; + +/** + * Parameters of the useAddDamageMode hook. + */ +export interface AddDamageModeParams extends Pick { + /** + * The current screen of the Capture component. + */ + currentScreen?: CaptureScreen; + /** + * Boolean indicating if the capture is in Damage Disclosure mode. + */ + damageDisclosure?: boolean; + /** + * Callback called when the user clicks on the 'close` button in Damage Diclosure mode. + */ + handleOpenGallery: () => void; +} + +/** + * Handle used to modify the current CaptureMode of the PhotoCapture or DamageDisclosure component. + */ +export interface AddDamageHandle { + /** + * The current mode of the component. + */ + mode: CaptureMode; + /** + * Parts selected to take a picture of. + */ + vehicleParts: VehiclePart[]; + /** + * Callback to be called when the user clicks on the "Add Damage" button. + */ + handleAddDamage: () => void; + /** + * Callback to be called when the user selects the parts to take a picture of. + */ + handleAddDamagePartsSelected: (parts: VehiclePart[]) => void; + /** + * Callback to be called everytime the user takes a picture to update the mode after it. + */ + updatePhotoCaptureModeAfterPictureTaken: () => void; + /** + * Callback to be called when the user clicks on the "Cancel" button of the Add Damage mode. + */ + handleCancelAddDamage: () => void; + /** + * Callback called when the user clicks on the "Validate" button of the Add Damage mode. + */ + handleValidateVehicleParts: () => void; +} + +function getInitialMode(addDamage?: AddDamage, damageDisclosure?: boolean): CaptureMode { + if (damageDisclosure && addDamage === AddDamage.PART_SELECT) { + return CaptureMode.ADD_DAMAGE_PART_SELECT; + } + if (damageDisclosure && addDamage === AddDamage.TWO_SHOT) { + return CaptureMode.ADD_DAMAGE_1ST_SHOT; + } + return CaptureMode.SIGHT; +} + +/** + * Custom hook used to switch between sight picture taking and add damage picture taking. + */ +export function useAddDamageMode({ + addDamage, + currentScreen, + handleOpenGallery = () => {}, + damageDisclosure = false, +}: AddDamageModeParams): AddDamageHandle { + const [mode, setMode] = useState(getInitialMode(addDamage, damageDisclosure)); + const [vehicleParts, setVehicleParts] = useState([]); + const { trackEvent } = useAnalytics(); + + const handleAddDamage = useCallback(() => { + if (addDamage === AddDamage.TWO_SHOT) { + setMode(CaptureMode.ADD_DAMAGE_1ST_SHOT); + trackEvent('AddDamage Selected', { + mode: CaptureMode.ADD_DAMAGE_1ST_SHOT, + }); + } + if (addDamage === AddDamage.PART_SELECT) { + setMode(CaptureMode.ADD_DAMAGE_PART_SELECT); + trackEvent('AddDamage Selected', { + mode: CaptureMode.ADD_DAMAGE_PART_SELECT, + }); + } + }, []); + + const updatePhotoCaptureModeAfterPictureTaken = useCallback(() => { + setVehicleParts([]); + setMode( + mode === CaptureMode.ADD_DAMAGE_1ST_SHOT + ? CaptureMode.ADD_DAMAGE_2ND_SHOT + : getInitialMode(addDamage, damageDisclosure), + ); + if (damageDisclosure) { + handleOpenGallery(); + } + }, [mode]); + + const handleCancelAddDamage = useCallback(() => { + trackEvent('AddDamage Canceled', { + mode, + }); + if ( + [CaptureMode.ADD_DAMAGE_PART_SELECT, CaptureMode.ADD_DAMAGE_1ST_SHOT].includes(mode) && + damageDisclosure + ) { + handleOpenGallery(); + } + setVehicleParts([]); + setMode(getInitialMode(addDamage, damageDisclosure)); + }, [mode, currentScreen]); + + const handleAddDamagePartsSelected = useCallback((parts: VehiclePart[]) => { + setVehicleParts(parts); + }, []); + + const handleValidateVehicleParts = useCallback(() => { + setMode(CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT); + }, []); + + return useObjectMemo({ + mode, + vehicleParts, + handleAddDamage, + updatePhotoCaptureModeAfterPictureTaken, + handleCancelAddDamage, + handleAddDamagePartsSelected, + handleValidateVehicleParts, + }); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/useBadConnectionWarning.ts b/packages/inspection-capture-web/src/hooks/useBadConnectionWarning.ts similarity index 100% rename from packages/inspection-capture-web/src/PhotoCapture/hooks/useBadConnectionWarning.ts rename to packages/inspection-capture-web/src/hooks/useBadConnectionWarning.ts diff --git a/packages/inspection-capture-web/src/hooks/useColorBackground.ts b/packages/inspection-capture-web/src/hooks/useColorBackground.ts new file mode 100644 index 000000000..502cbc52b --- /dev/null +++ b/packages/inspection-capture-web/src/hooks/useColorBackground.ts @@ -0,0 +1,13 @@ +import { useMonkTheme, changeAlpha } from '@monkvision/common'; +import { useMemo } from 'react'; + +/** + * Custom hook used to generate the background color in the inspection capture web components. + */ +export function useColorBackground(opacity = 0.64) { + const { palette } = useMonkTheme(); + + const clampedOpacity = Math.max(0, Math.min(opacity, 1)); + + return useMemo(() => changeAlpha(palette.background.base, opacity), [palette, clampedOpacity]); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts b/packages/inspection-capture-web/src/hooks/usePhotoCaptureImages.ts similarity index 84% rename from packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts rename to packages/inspection-capture-web/src/hooks/usePhotoCaptureImages.ts index 24de30bb2..bc355cbc6 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts +++ b/packages/inspection-capture-web/src/hooks/usePhotoCaptureImages.ts @@ -9,7 +9,7 @@ export function usePhotoCaptureImages(inspectionId: string): Image[] { const { state } = useMonkState(); return useMemo( - () => getInspectionImages(inspectionId, state.images, true), + () => getInspectionImages(inspectionId, state.images, undefined, true), [state.images, inspectionId], ); } diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePictureTaken.ts b/packages/inspection-capture-web/src/hooks/usePictureTaken.ts similarity index 53% rename from packages/inspection-capture-web/src/PhotoCapture/hooks/usePictureTaken.ts rename to packages/inspection-capture-web/src/hooks/usePictureTaken.ts index 6ef3b8f25..3b264e04d 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePictureTaken.ts +++ b/packages/inspection-capture-web/src/hooks/usePictureTaken.ts @@ -3,17 +3,17 @@ import { Queue } from '@monkvision/common'; import { useCallback } from 'react'; import { useAnalytics } from '@monkvision/analytics'; import { PictureUpload } from './useUploadQueue'; -import { AddDamageHandle, PhotoCaptureMode } from './useAddDamageMode'; -import { PhotoCaptureSightState } from './usePhotoCaptureSightState'; +import { AddDamageHandle } from './useAddDamageMode'; +import { PhotoCaptureSightState, DamageDisclosureState, CaptureMode } from '../types'; /** * Parameters of the usePictureTaken hook. */ export interface UseTakePictureParams { /** - * The PhotoCapture sight state, created using the usePhotoCaptureSightState hook. + * The capture state, created using the usePhotoCaptureSightState or useDamageDisclosureState hook. */ - sightState: PhotoCaptureSightState; + captureState: PhotoCaptureSightState | DamageDisclosureState; /** * The PhotoCapture add damage handle, created using the useAddDamageMode hook. */ @@ -42,29 +42,47 @@ export type HandleTakePictureFunction = (picture: MonkPicture) => void; * Custom hook used to generate the callback called when the user has taken a picture to handle picture upload etc. */ export function usePictureTaken({ - sightState, + captureState, addDamageHandle, uploadQueue, tasksBySight, onPictureTaken, }: UseTakePictureParams): HandleTakePictureFunction { const { trackEvent } = useAnalytics(); + + const selectedSightId = + 'selectedSight' in captureState ? captureState.selectedSight.id : undefined; + + const takeSelectedSight = + 'takeSelectedSight' in captureState ? captureState.takeSelectedSight : undefined; + return useCallback( (picture: MonkPicture) => { onPictureTaken?.(picture); - sightState.setLastPictureTakenUri(picture.uri); - const upload: PictureUpload = - addDamageHandle.mode === PhotoCaptureMode.SIGHT - ? { - mode: addDamageHandle.mode, - picture, - sightId: sightState.selectedSight.id, - tasks: tasksBySight?.[sightState.selectedSight.id] ?? sightState.selectedSight.tasks, - } - : { mode: addDamageHandle.mode, picture }; - uploadQueue.push(upload); - if (addDamageHandle.mode === PhotoCaptureMode.SIGHT) { - sightState.takeSelectedSight(); + captureState.setLastPictureTakenUri(picture.uri); + if (addDamageHandle.mode === CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT) { + uploadQueue.push({ + mode: addDamageHandle.mode, + picture, + vehicleParts: addDamageHandle.vehicleParts, + }); + } + if (addDamageHandle.mode === CaptureMode.SIGHT && 'selectedSight' in captureState) { + uploadQueue.push({ + mode: addDamageHandle.mode, + picture, + sightId: captureState.selectedSight.id, + tasks: tasksBySight?.[captureState.selectedSight.id] ?? captureState.selectedSight.tasks, + }); + } + if ( + addDamageHandle.mode === CaptureMode.ADD_DAMAGE_1ST_SHOT || + addDamageHandle.mode === CaptureMode.ADD_DAMAGE_2ND_SHOT + ) { + uploadQueue.push({ mode: addDamageHandle.mode, picture }); + } + if (addDamageHandle.mode === CaptureMode.SIGHT && 'takeSelectedSight' in captureState) { + captureState.takeSelectedSight(); } else { trackEvent('AddDamage Captured', { mode: addDamageHandle.mode, @@ -73,12 +91,12 @@ export function usePictureTaken({ addDamageHandle.updatePhotoCaptureModeAfterPictureTaken(); }, [ - sightState.setLastPictureTakenUri, + captureState.setLastPictureTakenUri, addDamageHandle.mode, - sightState.selectedSight.id, + selectedSightId, tasksBySight, uploadQueue.push, - sightState.takeSelectedSight, + takeSelectedSight, addDamageHandle.updatePhotoCaptureModeAfterPictureTaken, onPictureTaken, ], diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/useTracking.ts b/packages/inspection-capture-web/src/hooks/useTracking.ts similarity index 100% rename from packages/inspection-capture-web/src/PhotoCapture/hooks/useTracking.ts rename to packages/inspection-capture-web/src/hooks/useTracking.ts diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts b/packages/inspection-capture-web/src/hooks/useUploadQueue.ts similarity index 77% rename from packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts rename to packages/inspection-capture-web/src/hooks/useUploadQueue.ts index de9f06ddd..26a58d1d0 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts +++ b/packages/inspection-capture-web/src/hooks/useUploadQueue.ts @@ -1,9 +1,15 @@ import { Queue, uniq, useQueue } from '@monkvision/common'; import { AddImageOptions, ImageUploadType, MonkApiConfig, useMonkApi } from '@monkvision/network'; -import { PhotoCaptureAppConfig, ComplianceOptions, MonkPicture, TaskName } from '@monkvision/types'; +import { + PhotoCaptureAppConfig, + ComplianceOptions, + MonkPicture, + TaskName, + VehiclePart, +} from '@monkvision/types'; import { useRef } from 'react'; import { useMonitoring } from '@monkvision/monitoring'; -import { PhotoCaptureMode } from './useAddDamageMode'; +import { CaptureMode } from '../types'; /** * Type definition for upload event handlers. @@ -50,7 +56,7 @@ export interface SightPictureUpload { /** * Upload mode : `PhotoCaptureMode.SIGHT`. */ - mode: PhotoCaptureMode.SIGHT; + mode: CaptureMode.SIGHT; /** * The picture to upload. */ @@ -72,7 +78,7 @@ export interface AddDamage1stShotPictureUpload { /** * Upload mode : `PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT`. */ - mode: PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT; + mode: CaptureMode.ADD_DAMAGE_1ST_SHOT; /** * The picture to upload. */ @@ -86,20 +92,39 @@ export interface AddDamage2ndShotPictureUpload { /** * Upload mode : `PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT`. */ - mode: PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT; + mode: CaptureMode.ADD_DAMAGE_2ND_SHOT; /** * The picture to upload. */ picture: MonkPicture; } +/** + * Upload options for a part select picture in the add damage process. + */ +export interface AddDamagePartSelectShotPictureUpload { + /** + * Upload mode : `PhotoCaptureMode.ADD_DAMAGE_PART_SELECTION`. + */ + mode: CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT; + /** + * The picture to upload. + */ + picture: MonkPicture; + /** + * Selected damaged parts. + */ + vehicleParts: VehiclePart[]; +} + /** * Union type describing every possible upload configurations for a picture taken. */ export type PictureUpload = | SightPictureUpload | AddDamage1stShotPictureUpload - | AddDamage2ndShotPictureUpload; + | AddDamage2ndShotPictureUpload + | AddDamagePartSelectShotPictureUpload; function createAddImageOptions( upload: PictureUpload, @@ -109,7 +134,7 @@ function createAddImageOptions( additionalTasks?: PhotoCaptureAppConfig['additionalTasks'], compliance?: ComplianceOptions, ): AddImageOptions { - if (upload.mode === PhotoCaptureMode.SIGHT) { + if (upload.mode === CaptureMode.SIGHT) { return { uploadType: ImageUploadType.BEAUTY_SHOT, picture: upload.picture, @@ -120,11 +145,21 @@ function createAddImageOptions( useThumbnailCaching: enableThumbnail, }; } + if (upload.mode === CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT) { + return { + uploadType: ImageUploadType.PART_SELECT_SHOT, + picture: upload.picture, + inspectionId, + vehicleParts: upload.vehicleParts, + compliance, + useThumbnailCaching: enableThumbnail, + }; + } return { uploadType: ImageUploadType.CLOSE_UP_2_SHOT, picture: upload.picture, siblingKey: `closeup-sibling-key-${siblingId}`, - firstShot: upload.mode === PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT, + firstShot: upload.mode === CaptureMode.ADD_DAMAGE_1ST_SHOT, inspectionId, compliance, useThumbnailCaching: enableThumbnail, @@ -146,7 +181,7 @@ export function useUploadQueue({ const { addImage } = useMonkApi(apiConfig); return useQueue(async (upload: PictureUpload) => { - if (upload.mode === PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT) { + if (upload.mode === CaptureMode.ADD_DAMAGE_1ST_SHOT) { siblingIdRef.current += 1; } try { diff --git a/packages/inspection-capture-web/src/index.ts b/packages/inspection-capture-web/src/index.ts index 7d3d1a8df..bcd3448b9 100644 --- a/packages/inspection-capture-web/src/index.ts +++ b/packages/inspection-capture-web/src/index.ts @@ -1,3 +1,4 @@ export * from './PhotoCapture'; export * from './VideoCapture'; +export * from './DamageDisclosure'; export * from './i18n'; diff --git a/packages/inspection-capture-web/src/translations/de.json b/packages/inspection-capture-web/src/translations/de.json index fe0376ebc..56ac444e3 100644 --- a/packages/inspection-capture-web/src/translations/de.json +++ b/packages/inspection-capture-web/src/translations/de.json @@ -13,7 +13,9 @@ "damagedPartCounter": "1 / 2 • Beschädigtes Teil", "closeupPictureCounter": "2 / 2 • Nahaufnahme-Vorschau", "infoBtn": "Richte das Ziel auf den beschädigten Teil aus und tippe dann auf den Auslöserknopf", - "infoCloseup": "Ein Nahaufnahmebild von dem Schaden machen" + "infoCloseup": "Ein Nahaufnahmebild von dem Schaden machen", + "selectPart": "Wählen Sie auf den beschädigten Teil. Verwenden Sie die Pfeile, um das Fahrzeug zu drehen und die andere Seite zu sehen.", + "selectParts": "Wählen Sie auf die beschädigten Teile. Verwenden Sie die Pfeile, um das Fahrzeug zu drehen und die andere Seite zu sehen." }, "error": { "retry": "Erneut versuchen", @@ -39,6 +41,9 @@ "sight": "Richten Sie das Fahrzeug so gut wie möglich an den Linien aus, um das beste Foto zu erhalten.
Drücken Sie den Auslöser, um das Foto aufzunehmen.", "next": "Weiter" } + }, + "gallery": { + "next": "Weiter: fahrzeugfotos" } }, "video": { diff --git a/packages/inspection-capture-web/src/translations/en.json b/packages/inspection-capture-web/src/translations/en.json index 33f7c9bf1..32fa8fa26 100644 --- a/packages/inspection-capture-web/src/translations/en.json +++ b/packages/inspection-capture-web/src/translations/en.json @@ -13,7 +13,9 @@ "damagedPartCounter": "1 / 2 • Damaged part", "closeupPictureCounter": "2 / 2 • Closeup Picture", "infoBtn": "Aim the target at the damaged part then tap the shutter button", - "infoCloseup": "Take a closeup picture of the damage" + "infoCloseup": "Take a closeup picture of the damage", + "selectPart": "Tap the damaged part. Use arrows to rotate the vehicle and see the other side.", + "selectParts": "Tap the damaged parts. Use arrows to rotate the vehicle and see the other side." }, "error": { "retry": "Retry", @@ -39,6 +41,9 @@ "sight": "Align the vehicle with the lines as much as possible to get the best shot.
Press the shutter button to take the photo.", "next": "Next" } + }, + "gallery": { + "next": "Next: vehicle photos" } }, "video": { diff --git a/packages/inspection-capture-web/src/translations/fr.json b/packages/inspection-capture-web/src/translations/fr.json index 7208579fc..4ceadefe9 100644 --- a/packages/inspection-capture-web/src/translations/fr.json +++ b/packages/inspection-capture-web/src/translations/fr.json @@ -13,7 +13,9 @@ "damagedPartCounter": "1 / 2 • Pièce endommagée", "closeupPictureCounter": "2 / 2 • Dégât en gros plan", "infoBtn": "Placer le viseur sur la pièce endommagée puis enclencher le bouton capture de la photo", - "infoCloseup": "Prendre une photo en gros plan du dégât" + "infoCloseup": "Prendre une photo en gros plan du dégât", + "selectPart": "Veuillez sélectionner la pièce endommagée. Utilisez les flèches pour faire tourner le véhicule et voir l'autre côté", + "selectParts": "Veuillez sélectionner les pièces endommagées. Utilisez les flèches pour faire tourner le véhicule et voir l'autre côté." }, "error": { "retry": "Réessayer", @@ -39,6 +41,9 @@ "sight": "Alignez le véhicule avec les lignes autant que possible pour obtenir la meilleure photo.
Appuyez sur le déclencheur pour prendre la photo.", "next": "Suivant" } + }, + "gallery": { + "next": "Suivant : photos du véhicule" } }, "video": { diff --git a/packages/inspection-capture-web/src/translations/nl.json b/packages/inspection-capture-web/src/translations/nl.json index 27a5bfb34..4f61debb9 100644 --- a/packages/inspection-capture-web/src/translations/nl.json +++ b/packages/inspection-capture-web/src/translations/nl.json @@ -13,7 +13,9 @@ "damagedPartCounter": "1 / 2 • Beschadigd onderdeel", "closeupPictureCounter": "2 / 2 • Closeup foto", "infoBtn": "Richt de markering op het beschadigde onderdeel en tik vervolgens op de sluitertknop", - "infoCloseup": "Neem een closeup foto van de schade" + "infoCloseup": "Neem een closeup foto van de schade", + "selectPart": "Selecteer het beschadigde onderdeel. Gebruik de pijlen om het voertuig te draaien en de andere kant te bekijken.", + "selectParts": "Selecteer de beschadigde onderdelen. Gebruik de pijlen om het voertuig te draaien en de andere kant te bekijken." }, "error": { "retry": "Opnieuw proberen", @@ -39,6 +41,9 @@ "sight": "Richt het voertuig zo goed mogelijk uit op de lijnen om de beste foto te krijgen.
Druk op de knop om de foto te nemen.", "next": "Volgende" } + }, + "gallery": { + "next": "Volgende: voertuigfoto's" } }, "video": { diff --git a/packages/inspection-capture-web/src/types.ts b/packages/inspection-capture-web/src/types.ts new file mode 100644 index 000000000..66b57d369 --- /dev/null +++ b/packages/inspection-capture-web/src/types.ts @@ -0,0 +1,106 @@ +import { Sight } from '@monkvision/types'; +import { Dispatch, SetStateAction } from 'react'; + +/** + * Enum of the different picture taking modes that the PhotoCapture or DamageDisclosure component can be in. + */ +export enum CaptureMode { + /** + * SIGHT mode : user is asked to take a picture of its vehicle following a given Sight. + */ + SIGHT = 'sight', + /** + * ADD_DAMAGE_1ST_SHOT mode : user is asked to take a picture centered on a damage, far away from the vehicle. + */ + ADD_DAMAGE_1ST_SHOT = 'add_damage_1st_shot', + /** + * ADD_DAMAGE_2ND_SHOT mode : user is asked to take a zoomed picture of a damage on the car. + */ + ADD_DAMAGE_2ND_SHOT = 'add_damage_2nd_shot', + /** + * ADD_DAMAGE_PART_SELECT mode : user is asked to select car parts to take a picture of. + */ + ADD_DAMAGE_PART_SELECT = 'add_damage_part_select', + /** + * ADD_DAMAGE_PART_SELECT_SHOT mode : user is asked to take a close-up picture of a damage on the car part. + */ + ADD_DAMAGE_PART_SELECT_SHOT = 'add_damage_part_select_shot', +} + +/** + * Enum of the different capture screen available in the capture process. + */ +export enum CaptureScreen { + /** + * Camera scren. + */ + CAMERA = 'camera', + /** + * Gallery screen. + */ + GALLERY = 'gallery', +} + +/** + * Object containing state management utilities for the PhotoCapture sights. + */ +export interface PhotoCaptureSightState { + /** + * The currently selected sight in the PhotoCapture component : the sight that the user needs to capture. + */ + selectedSight: Sight; + /** + * Array containing the list of sights that the user has already captured. + */ + sightsTaken: Sight[]; + /** + * Callback called when the user manually select a new sight. + */ + selectSight: (s: Sight) => void; + /** + * Callback called when the user has taken a picture of a sight. + */ + takeSelectedSight: () => void; + /** + * Callback called when a sight needs to be retaken. + */ + retakeSight: (id: string) => void; + /** + * Value storing the last picture taken by the user. If no picture has been taken yet, this value is null. + */ + lastPictureTakenUri: string | null; + /** + * Callback used to manually update the last picture taken by the user after they take a picture. + */ + setLastPictureTakenUri: (uri: string) => void; + /** + * Callback that can be used to retry fetching this state object from the API in case the previous fetch failed. + */ + retryLoadingInspection: () => void; + /** + * Boolean indicating if the inspection is completed or not. + */ + isInspectionCompleted: boolean; + /** + * Callback used to manually update the completion state of the inspection. + */ + setIsInspectionCompleted: Dispatch>; +} + +/** + * Object containing state management utilities for the DamageDisclosure. + */ +export interface DamageDisclosureState { + /** + * Value storing the last picture taken by the user. If no picture has been taken yet, this value is null. + */ + lastPictureTakenUri: string | null; + /** + * Callback used to manually update the last picture taken by the user after they take a picture. + */ + setLastPictureTakenUri: (uri: string) => void; + /** + * Callback that can be used to retry fetching this state object from the API in case the previous fetch failed. + */ + retryLoadingInspection: () => void; +} diff --git a/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosure.test.tsx b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosure.test.tsx new file mode 100644 index 000000000..6f3d3b073 --- /dev/null +++ b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosure.test.tsx @@ -0,0 +1,354 @@ +import { + AddDamage, + CameraResolution, + ComplianceIssue, + CompressionFormat, + ImageType, + TaskName, + VehicleType, +} from '@monkvision/types'; + +const { CaptureMode } = jest.requireActual('../../src/types'); + +jest.mock('../../src/DamageDisclosure/hooks', () => ({ + useDamageDisclosureState: jest.fn(() => ({ + lastPictureTakenUri: 'test-uri-test', + setLastPictureTakenUri: jest.fn(), + retryLoadingInspection: jest.fn(), + })), +})); + +jest.mock('../../src/hooks', () => ({ + useAddDamageMode: jest.fn(() => ({ + mode: CaptureMode.SIGHT, + handleAddDamage: jest.fn(), + updatePhotoCaptureModeAfterPictureTaken: jest.fn(), + handleCancelAddDamage: jest.fn(), + })), + usePhotoCaptureImages: jest.fn(() => [{ id: 'test' }]), + usePictureTaken: jest.fn(() => jest.fn()), + useUploadQueue: jest.fn(() => ({ + length: 3, + })), + useBadConnectionWarning: jest.fn(() => ({ + isBadConnectionWarningDialogDisplayed: true, + closeBadConnectionWarningDialog: jest.fn(), + uploadEventHandlers: { + onUploadSuccess: jest.fn(), + onUploadTimeout: jest.fn(), + }, + })), + useAdaptiveCameraConfig: jest.fn(() => ({ + adaptiveCameraConfig: { + resolution: CameraResolution.QHD_2K, + format: CompressionFormat.JPEG, + allowImageUpscaling: false, + quality: 0.9, + }, + uploadEventHandlers: { + onUploadSuccess: jest.fn(), + onUploadTimeout: jest.fn(), + }, + })), + useTracking: jest.fn(), +})); +import { Camera } from '@monkvision/camera-web'; +import { useI18nSync, useLoadingState } from '@monkvision/common'; +import { BackdropDialog, InspectionGallery } from '@monkvision/common-ui-web'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { act, render } from '@testing-library/react'; +import { DamageDisclosure, DamageDisclosureHUD, DamageDisclosureProps } from '../../src'; +import { useDamageDisclosureState } from '../../src/DamageDisclosure/hooks'; +import { + useAdaptiveCameraConfig, + useAddDamageMode, + useBadConnectionWarning, + usePictureTaken, + useUploadQueue, + usePhotoCaptureImages, +} from '../../src/hooks'; + +function createProps(): DamageDisclosureProps { + return { + inspectionId: 'test-inspection-test', + apiConfig: { + apiDomain: 'test-api-domain-test', + authToken: 'test-auth-token-test', + thumbnailDomain: 'test-thumbnail-domain', + }, + enableCompliance: true, + useLiveCompliance: true, + complianceIssues: [ComplianceIssue.INTERIOR_NOT_SUPPORTED], + customComplianceThresholds: { blurriness: 0.3 }, + onClose: jest.fn(), + onComplete: jest.fn(), + onPictureTaken: jest.fn(), + resolution: CameraResolution.NHD_360P, + format: CompressionFormat.JPEG, + quality: 0.4, + showCloseButton: true, + lang: 'de', + allowImageUpscaling: true, + useAdaptiveImageQuality: false, + addDamage: AddDamage.PART_SELECT, + maxUploadDurationWarning: 456, + vehicleType: VehicleType.SEDAN, + }; +} + +describe('DamageDisclosure component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should pass the proper params to the useAdaptiveCameraConfig hook', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useAdaptiveCameraConfig).toHaveBeenCalledWith({ + initialCameraConfig: expect.objectContaining({ + quality: props.quality, + format: props.format, + resolution: props.resolution, + allowImageUpscaling: props.allowImageUpscaling, + }), + useAdaptiveImageQuality: props.useAdaptiveImageQuality, + }); + + unmount(); + }); + + it('should use adaptive image quality by default', () => { + const props = createProps(); + props.useAdaptiveImageQuality = undefined; + const { unmount } = render(); + + expect(useAdaptiveCameraConfig).toHaveBeenCalledWith( + expect.objectContaining({ + useAdaptiveImageQuality: true, + }), + ); + + unmount(); + }); + + it('should pass the proper params to the useDamageDisclosureState hooks', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useLoadingState).toHaveBeenCalled(); + const loading = (useLoadingState as jest.Mock).mock.results[0].value; + expect(useDamageDisclosureState).toHaveBeenCalledWith({ + inspectionId: props.inspectionId, + apiConfig: props.apiConfig, + loading, + complianceOptions: { + enableCompliance: props.enableCompliance, + useLiveCompliance: props.useLiveCompliance, + complianceIssues: props.complianceIssues, + customComplianceThresholds: props.customComplianceThresholds, + }, + }); + + unmount(); + }); + + it('should pass the proper params to the useBadConnectionWarning hooks', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useBadConnectionWarning).toHaveBeenCalledWith({ + maxUploadDurationWarning: props.maxUploadDurationWarning, + }); + + unmount(); + }); + + it('should pass the proper params to the useUploadQueue hook', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useAdaptiveCameraConfig).toHaveBeenCalled(); + const adaptiveConfigEventHandlers = (useAdaptiveCameraConfig as jest.Mock).mock.results[0].value + .uploadEventHandlers; + expect(useBadConnectionWarning).toHaveBeenCalled(); + const badConnectionEventHandlers = (useBadConnectionWarning as jest.Mock).mock.results[0].value + .uploadEventHandlers; + expect(useUploadQueue).toHaveBeenCalledWith({ + inspectionId: props.inspectionId, + apiConfig: props.apiConfig, + complianceOptions: { + enableCompliance: props.enableCompliance, + enableCompliancePerSight: props.enableCompliancePerSight, + complianceIssues: props.complianceIssues, + complianceIssuesPerSight: props.complianceIssuesPerSight, + useLiveCompliance: props.useLiveCompliance, + customComplianceThresholds: props.customComplianceThresholds, + customComplianceThresholdsPerSight: props.customComplianceThresholdsPerSight, + }, + eventHandlers: expect.arrayContaining([ + adaptiveConfigEventHandlers, + badConnectionEventHandlers, + ]), + }); + + unmount(); + }); + + it('should pass the proper params to the usePictureTaken hook', () => { + const props = { ...createProps(), tasksBySight: { test: [TaskName.PRICING] } }; + const { unmount } = render(); + + expect(useAddDamageMode).toHaveBeenCalled(); + const addDamageHandle = (useAddDamageMode as jest.Mock).mock.results[0].value; + expect(useDamageDisclosureState).toHaveBeenCalled(); + const captureState = (useDamageDisclosureState as jest.Mock).mock.results[0].value; + expect(useUploadQueue).toHaveBeenCalled(); + const uploadQueue = (useUploadQueue as jest.Mock).mock.results[0].value; + expect(usePictureTaken).toHaveBeenCalledWith({ + addDamageHandle, + captureState, + uploadQueue, + onPictureTaken: props.onPictureTaken, + }); + + unmount(); + }); + + it('should display a Camera with the config obtained from the useAdaptiveCameraConfig', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useAdaptiveCameraConfig).toHaveBeenCalled(); + const { adaptiveCameraConfig } = (useAdaptiveCameraConfig as jest.Mock).mock.results[0].value; + expectPropsOnChildMock(Camera, adaptiveCameraConfig); + + unmount(); + }); + + it('should use the PhotoCaptureHUD component as the Camera HUD', () => { + const props = createProps(); + const { unmount } = render(); + + expectPropsOnChildMock(Camera, { HUDComponent: DamageDisclosureHUD }); + + unmount(); + }); + + it('should pass the callback from the usePictureTaken hook to the Camera component', () => { + const props = createProps(); + const { unmount } = render(); + + expect(usePictureTaken).toHaveBeenCalled(); + const handlePictureTaken = (usePictureTaken as jest.Mock).mock.results[0].value; + expectPropsOnChildMock(Camera, { onPictureTaken: handlePictureTaken }); + + unmount(); + }); + + it('should pass the proper props to the HUD component', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useAddDamageMode).toHaveBeenCalled(); + const addDamageHandle = (useAddDamageMode as jest.Mock).mock.results[0].value; + expect(useDamageDisclosureState).toHaveBeenCalled(); + const disclosureState = (useDamageDisclosureState as jest.Mock).mock.results[0].value; + expect(useLoadingState).toHaveBeenCalled(); + const loading = (useLoadingState as jest.Mock).mock.results[0].value; + const images = (usePhotoCaptureImages as jest.Mock).mock.results[0].value; + expectPropsOnChildMock(Camera, { + hudProps: { + inspectionId: props.inspectionId, + mode: addDamageHandle.mode, + vehicleParts: addDamageHandle.vehicleParts, + lastPictureTakenUri: disclosureState.lastPictureTakenUri, + onOpenGallery: expect.any(Function), + onAddDamage: addDamageHandle.handleAddDamage, + onAddDamagePartsSelected: addDamageHandle.handleAddDamagePartsSelected, + onCancelAddDamage: addDamageHandle.handleCancelAddDamage, + loading, + onClose: props.onClose, + showCloseButton: props.showCloseButton, + images, + addDamage: props.addDamage, + onRetry: disclosureState.retryLoadingInspection, + onValidateVehicleParts: addDamageHandle.handleValidateVehicleParts, + vehicleType: props.vehicleType, + }, + }); + + unmount(); + }); + + it('should sync the local i18n language with the one passed as a prop', () => { + const props = createProps(); + props.lang = 'fr'; + const { unmount } = render(); + + expect(useI18nSync).toHaveBeenCalledWith(props.lang); + + unmount(); + }); + + it('should display the gallery when the gallery button is pressed', () => { + const props = createProps(); + const { unmount } = render(); + + expect(InspectionGallery).not.toHaveBeenCalled(); + expectPropsOnChildMock(Camera, { + hudProps: expect.objectContaining({ onOpenGallery: expect.any(Function) }), + }); + const { onOpenGallery } = (Camera as unknown as jest.Mock).mock.calls[0][0].hudProps; + expect(InspectionGallery).not.toHaveBeenCalled(); + act(() => onOpenGallery()); + expectPropsOnChildMock(InspectionGallery, { + inspectionId: props.inspectionId, + sights: [], + apiConfig: props.apiConfig, + captureMode: true, + lang: props.lang, + showBackButton: true, + onBack: expect.any(Function), + onNavigateToCapture: expect.any(Function), + onValidate: expect.any(Function), + addDamage: props.addDamage, + validateButtonLabel: 'photo.gallery.next', + isInspectionCompleted: false, + filterByImageType: ImageType.CLOSE_UP, + }); + const { onBack } = (InspectionGallery as unknown as jest.Mock).mock.calls[0][0]; + (InspectionGallery as unknown as jest.Mock).mockClear(); + (Camera as unknown as jest.Mock).mockClear(); + expect(Camera).not.toHaveBeenCalled(); + act(() => onBack()); + expect(InspectionGallery).not.toHaveBeenCalled(); + expect(Camera).toHaveBeenCalled(); + + unmount(); + }); + + it('should pass the proper params to the BackdropDialog component', () => { + const props = createProps(); + const { unmount } = render(); + + expect(useBadConnectionWarning).toHaveBeenCalled(); + const { isBadConnectionWarningDialogDisplayed, closeBadConnectionWarningDialog } = ( + useBadConnectionWarning as jest.Mock + ).mock.results[0].value; + expectPropsOnChildMock(BackdropDialog, { + show: isBadConnectionWarningDialogDisplayed, + dialogIcon: 'warning-outline', + dialogIconPrimaryColor: 'caution-base', + message: 'photo.badConnectionWarning.message', + confirmLabel: 'photo.badConnectionWarning.confirm', + onConfirm: expect.any(Function), + }); + const { onConfirm } = (BackdropDialog as unknown as jest.Mock).mock.calls[0][0]; + expect(closeBadConnectionWarningDialog).not.toHaveBeenCalled(); + onConfirm(); + expect(closeBadConnectionWarningDialog).toHaveBeenCalled(); + + unmount(); + }); +}); diff --git a/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements.test.tsx b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements.test.tsx new file mode 100644 index 000000000..1f0184879 --- /dev/null +++ b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements.test.tsx @@ -0,0 +1,108 @@ +jest.mock('../../../src/components', () => ({ + ZoomOutShot: jest.fn(() => <>), + CloseUpShot: jest.fn(() => <>), + PartSelection: jest.fn(() => <>), +})); + +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { AddDamage, VehicleType } from '@monkvision/types'; +import { CaptureMode } from '../../../src/types'; +import { DamageDisclosureHUDElements, DamageDisclosureHUDElementsProps } from '../../../src'; +import { ZoomOutShot, CloseUpShot, PartSelection } from '../../../src/components'; + +function createProps(): DamageDisclosureHUDElementsProps { + return { + mode: CaptureMode.SIGHT, + onAddDamage: jest.fn(), + onCancelAddDamage: jest.fn(), + previewDimensions: { height: 1234, width: 45678 }, + isLoading: false, + error: null, + onValidateVehicleParts: jest.fn(), + vehicleParts: [], + addDamage: AddDamage.PART_SELECT, + vehicleType: VehicleType.SEDAN, + }; +} + +describe('DamageDisclosureHUDElements component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return null if the capture is loading', () => { + const props = createProps(); + props.isLoading = true; + const { container, unmount } = render(); + + expect(container).toBeEmptyDOMElement(); + + unmount(); + }); + + it('should return null if the capture is in error', () => { + const props = createProps(); + props.error = true; + const { container, unmount } = render(); + + expect(container).toBeEmptyDOMElement(); + + unmount(); + }); + + it('should return the ZoomOutShot component if the mode is AD 1st Shot', () => { + const props = createProps(); + props.mode = CaptureMode.ADD_DAMAGE_1ST_SHOT; + const { unmount } = render(); + + expectPropsOnChildMock(ZoomOutShot, { + onCancel: props.onCancelAddDamage, + }); + expect(CloseUpShot).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should return the ClosuUpShot component if the mode is AD 2nd Shot', () => { + const props = createProps(); + props.mode = CaptureMode.ADD_DAMAGE_2ND_SHOT; + const { unmount } = render(); + + expectPropsOnChildMock(CloseUpShot, { + onCancel: props.onCancelAddDamage, + }); + expect(ZoomOutShot).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should return the ClosuUpShot component if the mode is AD PartSelect Shot', () => { + const props = createProps(); + props.mode = CaptureMode.ADD_DAMAGE_PART_SELECT_SHOT; + const { unmount } = render(); + + expectPropsOnChildMock(CloseUpShot, { + onCancel: props.onCancelAddDamage, + }); + expect(ZoomOutShot).not.toHaveBeenCalled(); + expect(PartSelection).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should return the PartSelection component if the mode is AD PartSelect', () => { + const props = createProps(); + props.mode = CaptureMode.ADD_DAMAGE_PART_SELECT; + const { unmount } = render(); + + expectPropsOnChildMock(PartSelection, { + onCancel: props.onCancelAddDamage, + }); + expect(ZoomOutShot).not.toHaveBeenCalled(); + expect(CloseUpShot).not.toHaveBeenCalled(); + + unmount(); + }); +}); diff --git a/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx new file mode 100644 index 000000000..9ccfcf079 --- /dev/null +++ b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx @@ -0,0 +1,166 @@ +import { Image, ImageStatus, VehicleType } from '@monkvision/types'; + +jest.mock('../../../src/components/HUDButtons', () => ({ + HUDButtons: jest.fn(() => <>), +})); +jest.mock('../../../src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUDElements', () => ({ + DamageDisclosureHUDElements: jest.fn(() => <>), +})); + +import { useTranslation } from 'react-i18next'; +import { act, render, screen } from '@testing-library/react'; +import { LoadingState } from '@monkvision/common'; +import { CameraHandle } from '@monkvision/camera-web'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { BackdropDialog } from '@monkvision/common-ui-web'; +import { + DamageDisclosureHUD, + DamageDisclosureHUDElements, + DamageDisclosureHUDProps, +} from '../../../src'; +import { HUDButtons } from '../../../src/components'; +import { CaptureMode } from '../../../src/types'; + +const cameraTestId = 'camera-test-id'; + +function createProps(): DamageDisclosureHUDProps { + return { + inspectionId: 'test-inspection-id-test', + lastPictureTakenUri: 'test-last-pic-taken', + mode: CaptureMode.SIGHT, + loading: { isLoading: false, error: null } as unknown as LoadingState, + onAddDamage: jest.fn(), + onCancelAddDamage: jest.fn(), + onRetry: jest.fn(), + onClose: jest.fn(), + onOpenGallery: jest.fn(), + showCloseButton: true, + handle: { + isLoading: false, + error: null, + dimensions: { height: 2, width: 4 }, + previewDimensions: { height: 111, width: 2222 }, + } as unknown as CameraHandle, + cameraPreview:
, + images: [{ sightId: 'test-sight-1', status: ImageStatus.NOT_COMPLIANT }] as Image[], + onValidateVehicleParts: jest.fn(), + vehicleParts: [], + vehicleType: VehicleType.SEDAN, + }; +} + +describe('DamageDisclosureHUD component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should display the camera preview on the screen', () => { + const props = createProps(); + const { unmount } = render(); + + expect(screen.queryByTestId(cameraTestId)).not.toBeNull(); + + unmount(); + }); + + it('should display the DamageDisclosureHUDElements component with the proper props', () => { + const props = createProps(); + const { unmount } = render(); + + expectPropsOnChildMock(DamageDisclosureHUDElements, { + mode: props.mode, + vehicleParts: props.vehicleParts, + onAddDamage: props.onAddDamage, + onCancelAddDamage: props.onCancelAddDamage, + onAddDamagePartsSelected: props.onAddDamagePartsSelected, + onValidateVehicleParts: props.onValidateVehicleParts, + isLoading: props.loading.isLoading || props.handle.isLoading, + error: props.loading.error ?? props.handle.error, + previewDimensions: props.handle.previewDimensions, + }); + + unmount(); + }); + + it('should display the HUDButtons component with the proper props', () => { + const props = createProps(); + const { unmount } = render(); + + expectPropsOnChildMock(HUDButtons, { + onTakePicture: props.handle?.takePicture, + galleryPreview: props.lastPictureTakenUri ?? undefined, + closeDisabled: !!props.loading.error || !!props.handle.error, + galleryDisabled: !!props.loading.error || !!props.handle.error, + takePictureDisabled: !!props.loading.error || !!props.handle.error, + showCloseButton: props.showCloseButton, + onOpenGallery: props.onOpenGallery, + }); + + unmount(); + }); + + it('should display the BackdropDialog component with the proper props', () => { + (useTranslation as jest.Mock).mockImplementationOnce(() => ({ t: jest.fn((v) => v) })); + const props = createProps(); + const { unmount } = render(); + + expectPropsOnChildMock(BackdropDialog, { + message: 'photo.hud.closeConfirm.message', + cancelLabel: 'photo.hud.closeConfirm.cancel', + confirmLabel: 'photo.hud.closeConfirm.confirm', + }); + + unmount(); + }); + + it('should properly handle the click on close event', () => { + const props = createProps(); + const { unmount } = render(); + + const { onClose } = (HUDButtons as jest.Mock).mock.calls[0][0]; + expectPropsOnChildMock(BackdropDialog, { show: false }); + jest.clearAllMocks(); + + act(() => onClose()); + expectPropsOnChildMock(BackdropDialog, { show: true }); + const { onConfirm } = (BackdropDialog as jest.Mock).mock.calls[0][0]; + jest.clearAllMocks(); + + expect(props.onClose).not.toHaveBeenCalled(); + act(() => onConfirm()); + expectPropsOnChildMock(BackdropDialog, { show: false }); + expect(props.onClose).toHaveBeenCalled(); + + unmount(); + }); + + const RETAKE_STATUSES = [ + ImageStatus.NOT_COMPLIANT, + ImageStatus.UPLOAD_FAILED, + ImageStatus.UPLOAD_ERROR, + ]; + + RETAKE_STATUSES.forEach((status) => { + it(`should display the gallery badge if there are images with the ${status} status`, () => { + const props = createProps(); + props.images = [{ status }, { status }, { status: 'test' }] as Image[]; + const { unmount } = render(); + + expectPropsOnChildMock(HUDButtons, { showGalleryBadge: false, retakeCount: 0 }); + + unmount(); + }); + }); + + it('should not display the gallery badge if there are no images with retake statuses', () => { + const props = createProps(); + props.images = Object.values(ImageStatus) + .filter((status) => !RETAKE_STATUSES.includes(status)) + .map((status) => ({ status } as Image)); + const { unmount } = render(); + + expectPropsOnChildMock(HUDButtons, { showGalleryBadge: false, retakeCount: 0 }); + + unmount(); + }); +}); diff --git a/packages/inspection-capture-web/test/DamageDisclosure/hooks/useDamageDisclosureState.test.ts b/packages/inspection-capture-web/test/DamageDisclosure/hooks/useDamageDisclosureState.test.ts new file mode 100644 index 000000000..60cafd64a --- /dev/null +++ b/packages/inspection-capture-web/test/DamageDisclosure/hooks/useDamageDisclosureState.test.ts @@ -0,0 +1,114 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { LoadingState, useAsyncEffect } from '@monkvision/common'; +import { ComplianceIssue } from '@monkvision/types'; +import { useMonitoring } from '@monkvision/monitoring'; +import { useMonkApi } from '@monkvision/network'; +import { act } from '@testing-library/react'; +import { + DamageDisclosureParams, + useDamageDisclosureState, +} from '../../../src/DamageDisclosure/hooks'; + +function createParams(): DamageDisclosureParams { + return { + inspectionId: 'test-inspection-id', + apiConfig: { + apiDomain: 'test-api-domain', + authToken: 'test-auth-token', + thumbnailDomain: 'test-thumbnail-domain', + }, + loading: { + start: jest.fn(), + onSuccess: jest.fn(), + onError: jest.fn(), + } as unknown as LoadingState, + complianceOptions: { + enableCompliance: true, + complianceIssues: [ComplianceIssue.INTERIOR_NOT_SUPPORTED], + enableCompliancePerSight: ['test-sight'], + complianceIssuesPerSight: { test: [ComplianceIssue.OVEREXPOSURE] }, + useLiveCompliance: true, + }, + }; +} + +describe('useDamageDisclosureSightState hook', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should properly initialize the state', () => { + const initialProps = createParams(); + const { result, unmount } = renderHook(useDamageDisclosureState, { initialProps }); + expect(result.current.lastPictureTakenUri).toBeNull(); + unmount(); + }); + + it('should start a request to fetch the inspection state from the API', () => { + const initialProps = createParams(); + const { unmount } = renderHook(useDamageDisclosureState, { initialProps }); + + expect(useMonkApi).toHaveBeenCalledWith(initialProps.apiConfig); + const getInspectionMock = (useMonkApi as jest.Mock).mock.results[0].value.getInspection; + expect(initialProps.loading.start).not.toHaveBeenCalled(); + expect(getInspectionMock).not.toHaveBeenCalled(); + expect(useAsyncEffect).toHaveBeenCalled(); + const effect = (useAsyncEffect as jest.Mock).mock.calls[0][0]; + effect(); + expect(initialProps.loading.start).toHaveBeenCalled(); + expect(getInspectionMock).toHaveBeenCalledWith({ + id: initialProps.inspectionId, + compliance: initialProps.complianceOptions, + }); + + unmount(); + }); + + it('should properly handle the error returned from the getInspection API call', () => { + const initialProps = createParams(); + const { unmount } = renderHook(useDamageDisclosureState, { initialProps }); + + expect(useMonitoring).toHaveBeenCalled(); + const handleErrorMock = (useMonitoring as jest.Mock).mock.results[0].value.handleError; + expect(handleErrorMock).not.toHaveBeenCalled(); + expect(initialProps.loading.onError).not.toHaveBeenCalled(); + expect(useAsyncEffect).toHaveBeenCalled(); + const { onReject } = (useAsyncEffect as jest.Mock).mock.calls[0][2]; + const err = { test: 'hello' }; + act(() => onReject(err)); + + expect(handleErrorMock).toHaveBeenCalledWith(err); + expect(initialProps.loading.onError).toHaveBeenCalledWith(err); + + unmount(); + }); + + it('should fetch the inspection state again when the inspectionId changes', () => { + const initialProps = createParams(); + const { unmount } = renderHook(useDamageDisclosureState, { initialProps }); + + expect(useAsyncEffect).toHaveBeenCalledWith( + expect.anything(), + expect.arrayContaining([initialProps.inspectionId]), + expect.anything(), + ); + + unmount(); + }); + + it('should fetch the inspection state again when the retry function is called', () => { + const initialProps = createParams(); + const { result, unmount } = renderHook(useDamageDisclosureState, { initialProps }); + + const retryDep = (useAsyncEffect as jest.Mock).mock.calls[0][1].filter( + (dep: any) => dep !== initialProps.inspectionId, + )[0]; + act(() => result.current.retryLoadingInspection()); + const newRetryDep = (useAsyncEffect as jest.Mock).mock.calls[1][1].filter( + (dep: any) => dep !== initialProps.inspectionId, + )[0]; + expect(Object.is(retryDep, newRetryDep)).toBe(false); + + unmount(); + }); +}); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx index d47cfb4b8..c92b23ac1 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx @@ -1,11 +1,13 @@ import { sights } from '@monkvision/sights'; import { + AddDamage, CameraResolution, ComplianceIssue, CompressionFormat, DeviceOrientation, PhotoCaptureTutorialOption, TaskName, + VehicleType, } from '@monkvision/types'; import { Camera } from '@monkvision/camera-web'; import { useI18nSync, useLoadingState, usePreventExit } from '@monkvision/common'; @@ -14,27 +16,20 @@ import { useMonitoring } from '@monkvision/monitoring'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { act, render, waitFor } from '@testing-library/react'; import { PhotoCapture, PhotoCaptureHUD, PhotoCaptureProps } from '../../src'; +import { usePhotoCaptureSightState, usePhotoCaptureTutorial } from '../../src/PhotoCapture/hooks'; import { + useStartTasksOnComplete, useAdaptiveCameraConfig, useAddDamageMode, useBadConnectionWarning, usePhotoCaptureImages, - usePhotoCaptureSightState, - usePhotoCaptureTutorial, usePictureTaken, useUploadQueue, -} from '../../src/PhotoCapture/hooks'; -import { useStartTasksOnComplete } from '../../src/hooks'; +} from '../../src/hooks'; -const { PhotoCaptureMode } = jest.requireActual('../../src/PhotoCapture/hooks'); +const { CaptureMode } = jest.requireActual('../../src/types'); jest.mock('../../src/PhotoCapture/hooks', () => ({ - useAddDamageMode: jest.fn(() => ({ - mode: PhotoCaptureMode.SIGHT, - handleAddDamage: jest.fn(), - updatePhotoCaptureModeAfterPictureTaken: jest.fn(), - handleCancelAddDamage: jest.fn(), - })), usePhotoCaptureSightState: jest.fn(() => ({ selectedSight: sights['test-sight-2'], sightsTaken: [sights['test-sight-1']], @@ -44,14 +39,28 @@ jest.mock('../../src/PhotoCapture/hooks', () => ({ setLastPictureTakenUri: jest.fn(), retryLoadingInspection: jest.fn(), })), - usePictureTaken: jest.fn(() => jest.fn()), - useUploadQueue: jest.fn(() => ({ - length: 3, + useComplianceAnalytics: jest.fn(() => ({ + isInitialInspectionFetched: jest.fn(), })), + usePhotoCaptureTutorial: jest.fn(() => ({ + currentTutorialStep: 'welcome', + goToNextTutorialStep: jest.fn(), + closeTutorial: jest.fn(), + })), +})); + +jest.mock('../../src/hooks', () => ({ useStartTasksOnComplete: jest.fn(() => jest.fn()), + useAddDamageMode: jest.fn(() => ({ + mode: CaptureMode.SIGHT, + handleAddDamage: jest.fn(), + updatePhotoCaptureModeAfterPictureTaken: jest.fn(), + handleCancelAddDamage: jest.fn(), + })), usePhotoCaptureImages: jest.fn(() => [{ id: 'test' }]), - useComplianceAnalytics: jest.fn(() => ({ - isInitialInspectionFetched: jest.fn(), + usePictureTaken: jest.fn(() => jest.fn()), + useUploadQueue: jest.fn(() => ({ + length: 3, })), useBadConnectionWarning: jest.fn(() => ({ isBadConnectionWarningDialogDisplayed: true, @@ -74,15 +83,6 @@ jest.mock('../../src/PhotoCapture/hooks', () => ({ }, })), useTracking: jest.fn(), - usePhotoCaptureTutorial: jest.fn(() => ({ - currentTutorialStep: 'welcome', - goToNextTutorialStep: jest.fn(), - closeTutorial: jest.fn(), - })), -})); - -jest.mock('../../src/hooks', () => ({ - useStartTasksOnComplete: jest.fn(() => jest.fn()), })); function createProps(): PhotoCaptureProps { @@ -116,7 +116,7 @@ function createProps(): PhotoCaptureProps { allowImageUpscaling: true, useAdaptiveImageQuality: false, allowSkipRetake: true, - enableAddDamage: true, + addDamage: AddDamage.PART_SELECT, maxUploadDurationWarning: 456, enableSightGuidelines: true, sightGuidelines: [ @@ -131,6 +131,7 @@ function createProps(): PhotoCaptureProps { enableTutorial: PhotoCaptureTutorialOption.ENABLED, allowSkipTutorial: true, enableSightTutorial: true, + vehicleType: VehicleType.SEDAN, }; } @@ -266,12 +267,12 @@ describe('PhotoCapture component', () => { expect(useAddDamageMode).toHaveBeenCalled(); const addDamageHandle = (useAddDamageMode as jest.Mock).mock.results[0].value; expect(usePhotoCaptureSightState).toHaveBeenCalled(); - const sightState = (usePhotoCaptureSightState as jest.Mock).mock.results[0].value; + const captureState = (usePhotoCaptureSightState as jest.Mock).mock.results[0].value; expect(useUploadQueue).toHaveBeenCalled(); const uploadQueue = (useUploadQueue as jest.Mock).mock.results[0].value; expect(usePictureTaken).toHaveBeenCalledWith({ addDamageHandle, - sightState, + captureState, uploadQueue, tasksBySight: props.tasksBySight, onPictureTaken: props.onPictureTaken, @@ -341,7 +342,7 @@ describe('PhotoCapture component', () => { showCloseButton: props.showCloseButton, onOpenGallery: expect.any(Function), images, - enableAddDamage: props.enableAddDamage, + addDamage: props.addDamage, enableSightGuidelines: props.enableSightGuidelines, sightGuidelines: props.sightGuidelines, currentTutorialStep: tutorial.currentTutorialStep, @@ -349,6 +350,7 @@ describe('PhotoCapture component', () => { onCloseTutorial: tutorial.closeTutorial, allowSkipTutorial: props.allowSkipRetake, enforceOrientation: props.enforceOrientation, + vehicleType: props.vehicleType, }, }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx index 969e6dc79..9cf632914 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx @@ -1,37 +1,27 @@ -import { DeviceOrientation, Image, ImageStatus } from '@monkvision/types'; -import { useTranslation } from 'react-i18next'; -import { act, render, screen } from '@testing-library/react'; -import { sights } from '@monkvision/sights'; -import { LoadingState } from '@monkvision/common'; -import { CameraHandle } from '@monkvision/camera-web'; -import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { BackdropDialog } from '@monkvision/common-ui-web'; -import { - PhotoCaptureHUD, - PhotoCaptureHUDButtons, - PhotoCaptureHUDElements, - PhotoCaptureHUDOverlay, - PhotoCaptureHUDProps, -} from '../../../src'; -import { PhotoCaptureMode } from '../../../src/PhotoCapture/hooks'; -import { OrientationEnforcer } from '../../../src/components'; - jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/hooks', () => ({ ...jest.requireActual('../../../src/PhotoCapture/PhotoCaptureHUD/hooks'), useComplianceNotification: jest.fn(() => false), })); -jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons', () => ({ - PhotoCaptureHUDButtons: jest.fn(() => <>), -})); -jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay', () => ({ - PhotoCaptureHUDOverlay: jest.fn(() => <>), +jest.mock('../../../src/components', () => ({ + HUDButtons: jest.fn(() => <>), + HUDOverlay: jest.fn(() => <>), + OrientationEnforcer: jest.fn(() => <>), })); jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements', () => ({ PhotoCaptureHUDElements: jest.fn(() => <>), })); -jest.mock('../../../src/components', () => ({ - OrientationEnforcer: jest.fn(() => <>), -})); + +import { useTranslation } from 'react-i18next'; +import { act, render, screen } from '@testing-library/react'; +import { sights } from '@monkvision/sights'; +import { LoadingState } from '@monkvision/common'; +import { CameraHandle } from '@monkvision/camera-web'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { BackdropDialog } from '@monkvision/common-ui-web'; +import { PhotoCaptureHUD, PhotoCaptureHUDElements, PhotoCaptureHUDProps } from '../../../src'; +import { HUDButtons, HUDOverlay, OrientationEnforcer } from '../../../src/components'; +import { CaptureMode } from '../../../src/types'; +import { ImageStatus, Image, DeviceOrientation, VehicleType } from '@monkvision/types'; const cameraTestId = 'camera-test-id'; @@ -47,7 +37,7 @@ function createProps(): PhotoCaptureHUDProps { selectedSight: sights['test-sight-2'], sightsTaken: [sights['test-sight-1']], lastPictureTakenUri: 'test-last-pic-taken', - mode: PhotoCaptureMode.SIGHT, + mode: CaptureMode.SIGHT, loading: { isLoading: false, error: null } as unknown as LoadingState, onSelectSight: jest.fn(), onRetakeSight: jest.fn(), @@ -70,6 +60,9 @@ function createProps(): PhotoCaptureHUDProps { onNextTutorialStep: jest.fn(), onCloseTutorial: jest.fn(), enforceOrientation: DeviceOrientation.PORTRAIT, + onValidateVehicleParts: jest.fn(), + vehicleParts: [], + vehicleType: VehicleType.SEDAN, }; } @@ -112,7 +105,7 @@ describe('PhotoCaptureHUD component', () => { const props = createProps(); const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDButtons, { + expectPropsOnChildMock(HUDButtons, { onTakePicture: props.handle?.takePicture, galleryPreview: props.lastPictureTakenUri ?? undefined, closeDisabled: !!props.loading.error || !!props.handle.error, @@ -125,11 +118,11 @@ describe('PhotoCaptureHUD component', () => { unmount(); }); - it('should display the PhotoCaptureHUDOverlay component with the proper props', () => { + it('should display the HUDOverlay component with the proper props', () => { const props = createProps(); const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDOverlay, { + expectPropsOnChildMock(HUDOverlay, { inspectionId: props.inspectionId, handle: props.handle, isCaptureLoading: props.loading.isLoading, @@ -158,7 +151,7 @@ describe('PhotoCaptureHUD component', () => { const props = createProps(); const { unmount } = render(); - const { onClose } = (PhotoCaptureHUDButtons as jest.Mock).mock.calls[0][0]; + const { onClose } = (HUDButtons as jest.Mock).mock.calls[0][0]; expectPropsOnChildMock(BackdropDialog, { show: false }); jest.clearAllMocks(); @@ -187,7 +180,7 @@ describe('PhotoCaptureHUD component', () => { props.images = [{ status }, { status }, { status: 'test' }] as Image[]; const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDButtons, { showGalleryBadge: true, retakeCount: 2 }); + expectPropsOnChildMock(HUDButtons, { showGalleryBadge: true, retakeCount: 2 }); unmount(); }); @@ -200,7 +193,7 @@ describe('PhotoCaptureHUD component', () => { .map((status) => ({ status } as Image)); const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDButtons, { showGalleryBadge: false, retakeCount: 0 }); + expectPropsOnChildMock(HUDButtons, { showGalleryBadge: false, retakeCount: 0 }); unmount(); }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements.test.tsx index 1b9d8a475..dbd196be7 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements.test.tsx @@ -1,33 +1,26 @@ -import { Image, ImageStatus } from '@monkvision/types'; +import { AddDamage, Image, ImageStatus, VehicleType } from '@monkvision/types'; jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight', () => ({ PhotoCaptureHUDElementsSight: jest.fn(() => <>), })); -jest.mock( - '../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot', - () => ({ - PhotoCaptureHUDElementsAddDamage1stShot: jest.fn(() => <>), - }), -); -jest.mock( - '../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot', - () => ({ - PhotoCaptureHUDElementsAddDamage2ndShot: jest.fn(() => <>), - }), -); +jest.mock('../../../src/components/ZoomOutShot', () => ({ + ZoomOutShot: jest.fn(() => <>), +})); +jest.mock('../../../src/components/CloseUpShot', () => ({ + CloseUpShot: jest.fn(() => <>), +})); import { sights } from '@monkvision/sights'; import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { PhotoCaptureMode } from '../../../src/PhotoCapture/hooks'; +import { CaptureMode } from '../../../src/types'; import { PhotoCaptureHUDElements, - PhotoCaptureHUDElementsAddDamage1stShot, - PhotoCaptureHUDElementsAddDamage2ndShot, PhotoCaptureHUDElementsProps, PhotoCaptureHUDElementsSight, } from '../../../src'; +import { ZoomOutShot, CloseUpShot } from '../../../src/components'; function createProps(): PhotoCaptureHUDElementsProps { const captureSights = [sights['test-sight-1'], sights['test-sight-2'], sights['test-sight-3']]; @@ -35,7 +28,7 @@ function createProps(): PhotoCaptureHUDElementsProps { selectedSight: captureSights[1], sights: captureSights, sightsTaken: [captureSights[0]], - mode: PhotoCaptureMode.SIGHT, + mode: CaptureMode.SIGHT, onAddDamage: jest.fn(), onCancelAddDamage: jest.fn(), onSelectSight: jest.fn(), @@ -45,6 +38,10 @@ function createProps(): PhotoCaptureHUDElementsProps { error: null, images: [{ sightId: 'test-sight-1', status: ImageStatus.NOT_COMPLIANT }] as Image[], tutorialStep: null, + onValidateVehicleParts: jest.fn(), + vehicleParts: [], + addDamage: AddDamage.PART_SELECT, + vehicleType: VehicleType.SEDAN, }; } @@ -75,7 +72,7 @@ describe('PhotoCaptureHUDElements component', () => { it('should return the PhotoCaptureHUDElementsSight component if the mode is Sight', () => { const props = createProps(); - props.mode = PhotoCaptureMode.SIGHT; + props.mode = CaptureMode.SIGHT; const { unmount } = render(); expectPropsOnChildMock(PhotoCaptureHUDElementsSight, { @@ -87,36 +84,36 @@ describe('PhotoCaptureHUDElements component', () => { previewDimensions: props.previewDimensions, images: props.images, }); - expect(PhotoCaptureHUDElementsAddDamage1stShot).not.toHaveBeenCalled(); - expect(PhotoCaptureHUDElementsAddDamage2ndShot).not.toHaveBeenCalled(); + expect(ZoomOutShot).not.toHaveBeenCalled(); + expect(CloseUpShot).not.toHaveBeenCalled(); unmount(); }); it('should return the PhotoCaptureHUDElementsAddDamage1stShot component if the mode is AD 1st Shot', () => { const props = createProps(); - props.mode = PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT; + props.mode = CaptureMode.ADD_DAMAGE_1ST_SHOT; const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDElementsAddDamage1stShot, { + expectPropsOnChildMock(ZoomOutShot, { onCancel: props.onCancelAddDamage, }); expect(PhotoCaptureHUDElementsSight).not.toHaveBeenCalled(); - expect(PhotoCaptureHUDElementsAddDamage2ndShot).not.toHaveBeenCalled(); + expect(CloseUpShot).not.toHaveBeenCalled(); unmount(); }); it('should return the PhotoCaptureHUDElementsAddDamage1stShot component if the mode is AD 2nd Shot', () => { const props = createProps(); - props.mode = PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT; + props.mode = CaptureMode.ADD_DAMAGE_2ND_SHOT; const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDElementsAddDamage2ndShot, { + expectPropsOnChildMock(CloseUpShot, { onCancel: props.onCancelAddDamage, }); expect(PhotoCaptureHUDElementsSight).not.toHaveBeenCalled(); - expect(PhotoCaptureHUDElementsAddDamage1stShot).not.toHaveBeenCalled(); + expect(ZoomOutShot).not.toHaveBeenCalled(); unmount(); }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx index 07c9ed665..54bff3cd3 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx @@ -3,12 +3,13 @@ import { useTranslation } from 'react-i18next'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { Button } from '@monkvision/common-ui-web'; import { AddDamageButton } from '../../../../src'; +import { AddDamage } from '@monkvision/types'; describe('AddDamageButton component', () => { - it('should not render when enableAddDamage is false', () => { + it('should not render when addDamage is disabled', () => { const onAddDamage = jest.fn(); const { unmount } = render( - , + , ); expect(Button).not.toHaveBeenCalled(); @@ -18,7 +19,7 @@ describe('AddDamageButton component', () => { it('should pass the onAddDamage callback to the onClick event of the Button', () => { const onAddDamage = jest.fn(); const { unmount } = render( - , + , ); expectPropsOnChildMock(Button, { onClick: onAddDamage }); @@ -30,7 +31,7 @@ describe('AddDamageButton component', () => { const label = 'test-label-ok'; const tMock = jest.fn(() => label); (useTranslation as jest.Mock).mockImplementationOnce(() => ({ t: tMock })); - const { unmount } = render(); + const { unmount } = render(); expect(tMock).toHaveBeenCalledWith('photo.hud.sight.addDamageBtn'); expectPropsOnChildMock(Button, { children: label }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx index bac9cc830..5353439f0 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx @@ -1,7 +1,7 @@ import { Image, ImageStatus } from '@monkvision/types'; -jest.mock('../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter', () => ({ - PhotoCaptureHUDCounter: jest.fn(() => <>), +jest.mock('../../../../src/components/Counter', () => ({ + Counter: jest.fn(() => <>), })); jest.mock( '../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton', @@ -22,12 +22,13 @@ import { SightOverlay } from '@monkvision/common-ui-web'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { AddDamageButton, - PhotoCaptureHUDCounter, PhotoCaptureHUDElementsSight, PhotoCaptureHUDElementsSightProps, SightSlider, } from '../../../../src'; -import { PhotoCaptureMode, TutorialSteps } from '../../../../src/PhotoCapture/hooks'; +import { Counter } from '../../../../src/components'; +import { CaptureMode } from '../../../../src/types'; +import { TutorialSteps } from '../../../../src/PhotoCapture/hooks'; function createProps(): PhotoCaptureHUDElementsSightProps { const captureSights = [ @@ -69,8 +70,8 @@ describe('PhotoCaptureHUDElementsSight component', () => { , ); - expectPropsOnChildMock(PhotoCaptureHUDCounter, { - mode: PhotoCaptureMode.SIGHT, + expectPropsOnChildMock(Counter, { + mode: CaptureMode.SIGHT, totalSights: props.sights.length, sightsTaken: props.sightsTaken.length, }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx index 43604bef9..b5822c0ee 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx @@ -14,6 +14,7 @@ import { } from '../../../../src'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { TutorialSteps } from '../../../../src/PhotoCapture/hooks'; +import { AddDamage } from '@monkvision/types'; const BACKDROP_TEST_ID = 'backdrop'; const TITLE_TEST_ID = 'title'; @@ -27,6 +28,7 @@ function createProps(): PhotoCaptureHUDTutorialProps { allowSkipTutorial: false, sightGuidelines: [], sightId: 'test-sight-id', + addDamage: AddDamage.PART_SELECT, }; } @@ -57,7 +59,7 @@ describe('PhotoCaptureHUDTutorial component', () => { sightId: props.sightId, sightGuidelines: props.sightGuidelines, enableSightGuidelines: props.currentTutorialStep === TutorialSteps.GUIDELINE, - enableAddDamage: true, + addDamage: AddDamage.PART_SELECT, }); expect(DynamicSVG).not.toHaveBeenCalled(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts deleted file mode 100644 index d819ec617..000000000 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { PhotoCaptureMode, useAddDamageMode } from '../../../src/PhotoCapture/hooks'; -import { act } from '@testing-library/react'; - -describe('useAddDamageMode hook', () => { - it('should be in the SIGHT mode by default', () => { - const { result, unmount } = renderHook(useAddDamageMode); - expect(result.current.mode).toEqual(PhotoCaptureMode.SIGHT); - unmount(); - }); - - it('should switch to ADD_DAMAGE_1ST_SHOT', () => { - const { result, unmount } = renderHook(useAddDamageMode); - act(() => result.current.handleAddDamage()); - expect(result.current.mode).toEqual(PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT); - unmount(); - }); - - it('should switch to ADD_DAMAGE_2ND_SHOT', () => { - const { result, unmount } = renderHook(useAddDamageMode); - act(() => result.current.handleAddDamage()); - act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); - expect(result.current.mode).toEqual(PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT); - unmount(); - }); - - it('should go back to the SIGHT mode', () => { - const { result, unmount } = renderHook(useAddDamageMode); - act(() => result.current.handleAddDamage()); - act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); - act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); - expect(result.current.mode).toEqual(PhotoCaptureMode.SIGHT); - unmount(); - }); - - it('should allow to cancel add damage at any time', () => { - const { result, unmount } = renderHook(useAddDamageMode); - act(() => result.current.handleAddDamage()); - act(() => result.current.handleCancelAddDamage()); - expect(result.current.mode).toEqual(PhotoCaptureMode.SIGHT); - act(() => result.current.handleAddDamage()); - act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); - act(() => result.current.handleCancelAddDamage()); - expect(result.current.mode).toEqual(PhotoCaptureMode.SIGHT); - unmount(); - }); -}); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton.test.tsx b/packages/inspection-capture-web/test/components/CancelButton.test.tsx similarity index 75% rename from packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton.test.tsx rename to packages/inspection-capture-web/test/components/CancelButton.test.tsx index 60f6d36f3..aa92158eb 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton.test.tsx +++ b/packages/inspection-capture-web/test/components/CancelButton.test.tsx @@ -2,9 +2,9 @@ import { render } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { Button } from '@monkvision/common-ui-web'; import { useTranslation } from 'react-i18next'; -import { PhotoCaptureHUDCancelButton } from '../../../src'; +import { CancelButton } from '../../src/components'; -describe('PhotoCaptureHUDCancelButton component', () => { +describe('CancelButton component', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -13,7 +13,7 @@ describe('PhotoCaptureHUDCancelButton component', () => { const label = 'wow-test-label-yes'; const tMock = jest.fn(() => label); (useTranslation as jest.Mock).mockImplementationOnce(() => ({ t: tMock })); - const { unmount } = render(); + const { unmount } = render(); expect(tMock).toHaveBeenCalledWith('photo.hud.addDamage.cancelBtn'); expectPropsOnChildMock(Button, { children: label }); @@ -23,7 +23,7 @@ describe('PhotoCaptureHUDCancelButton component', () => { it('should pass the onClick event to the Button', () => { const onCancel = jest.fn(); - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(Button, { onClick: onCancel }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot.test.tsx b/packages/inspection-capture-web/test/components/CloseUpShot.test.tsx similarity index 53% rename from packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot.test.tsx rename to packages/inspection-capture-web/test/components/CloseUpShot.test.tsx index 6fef9bb6a..729c75009 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage2ndShot.test.tsx +++ b/packages/inspection-capture-web/test/components/CloseUpShot.test.tsx @@ -1,32 +1,29 @@ -jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter', () => ({ - PhotoCaptureHUDCounter: jest.fn(() => <>), +const streamDimensions = { width: 5436, height: 1231 }; + +jest.mock('../../src/components/Counter', () => ({ + Counter: jest.fn(() => <>), +})); +jest.mock('../../src/components/CancelButton', () => ({ + CancelButton: jest.fn(() => <>), })); -jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton', () => ({ - PhotoCaptureHUDCancelButton: jest.fn(() => <>), +jest.mock('@monkvision/common', () => ({ + ...jest.requireActual('@monkvision/common'), + getAspectRatio: jest.fn(() => `${streamDimensions?.width}/${streamDimensions?.height}`), })); import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { useTranslation } from 'react-i18next'; -import { - PhotoCaptureHUDCancelButton, - PhotoCaptureHUDCounter, - PhotoCaptureHUDElementsAddDamage2ndShot, -} from '../../../src'; -import { isMobileDevice } from '@monkvision/common'; -import { PhotoCaptureMode } from '../../../src/PhotoCapture/hooks'; +import { CancelButton, Counter, CloseUpShot } from '../../src/components'; +import { getAspectRatio } from '@monkvision/common'; +import { CaptureMode } from '../../src/types'; const FRAME_CONTAINER_TEST_ID = 'frame-container'; -describe('PhotoCaptureHUDElementsAddDamage2ndShot component', () => { +describe('CloseUpShot component', () => { it('should display a frame on the center of the screen', () => { - const isMobileDeviceMock = isMobileDevice as jest.Mock; - isMobileDeviceMock.mockReturnValue(true); - const streamDimensions = { width: 5436, height: 1231 }; - const { unmount } = render( - , - ); + const { unmount } = render(); const frameContainerEl = screen.getByTestId(FRAME_CONTAINER_TEST_ID); expect(frameContainerEl.style.aspectRatio).toEqual( @@ -52,27 +49,29 @@ describe('PhotoCaptureHUDElementsAddDamage2ndShot component', () => { }); it('should give the frame container a 16:9 ratio by default if the stream dimensions are not defined', () => { - const { unmount } = render(); + (getAspectRatio as jest.Mock).mockImplementation(() => '16/9'); + const { unmount } = render(); const frameContainerEl = screen.getByTestId(FRAME_CONTAINER_TEST_ID); + expect(frameContainerEl.style.aspectRatio).toEqual('16/9'); unmount(); }); - it('should display the PhotoCaptureHUDCounter in AD 2nd Shot mode', () => { - const { unmount } = render(); + it('should display the Counter in AD 2nd Shot mode', () => { + const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDCounter, { mode: PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT }); + expectPropsOnChildMock(Counter, { mode: CaptureMode.ADD_DAMAGE_2ND_SHOT }); unmount(); }); it('should display the a cancel button component and pass it the onCancel prop', () => { const onCancel = jest.fn(); - const { unmount } = render(); + const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDCancelButton, { onCancel }); + expectPropsOnChildMock(CancelButton, { onCancel }); unmount(); }); @@ -81,10 +80,9 @@ describe('PhotoCaptureHUDElementsAddDamage2ndShot component', () => { const label = 'test'; const tMock = jest.fn(() => label); (useTranslation as jest.Mock).mockImplementationOnce(() => ({ t: tMock })); - const { unmount } = render(); + const { unmount } = render(); expect(tMock).toHaveBeenCalledWith('photo.hud.addDamage.infoCloseup'); - expect(screen.queryByText(label)).not.toBeNull(); unmount(); }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter.test.tsx b/packages/inspection-capture-web/test/components/Counter.test.tsx similarity index 65% rename from packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter.test.tsx rename to packages/inspection-capture-web/test/components/Counter.test.tsx index 1113addd3..d364d86b7 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter.test.tsx +++ b/packages/inspection-capture-web/test/components/Counter.test.tsx @@ -1,9 +1,9 @@ import { render, screen } from '@testing-library/react'; -import { PhotoCaptureHUDCounter } from '../../../src'; -import { PhotoCaptureMode } from '../../../src/PhotoCapture/hooks'; import { useTranslation } from 'react-i18next'; +import { Counter } from '../../src/components'; +import { CaptureMode } from '../../src/types'; -describe('PhotoCaptureHUDCounter component', () => { +describe('Counter component', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -12,11 +12,7 @@ describe('PhotoCaptureHUDCounter component', () => { const totalSights = 51; const sightsTaken = 12; const { unmount } = render( - , + , ); expect(screen.queryByText(`${sightsTaken} / ${totalSights}`)).not.toBeNull(); @@ -28,14 +24,12 @@ describe('PhotoCaptureHUDCounter component', () => { const label = 'fake-label'; const tMock = jest.fn(() => label); (useTranslation as jest.Mock).mockImplementation(() => ({ t: tMock })); - const { unmount, rerender } = render( - , - ); + const { unmount, rerender } = render(); expect(tMock).toHaveBeenCalledWith('photo.hud.addDamage.damagedPartCounter'); expect(screen.queryByText(label)).not.toBeNull(); - rerender(); + rerender(); expect(tMock).toHaveBeenCalledWith('photo.hud.addDamage.closeupPictureCounter'); expect(screen.queryByText(label)).not.toBeNull(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons.test.tsx b/packages/inspection-capture-web/test/components/HUDButtons.test.tsx similarity index 75% rename from packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons.test.tsx rename to packages/inspection-capture-web/test/components/HUDButtons.test.tsx index 7b1955f1f..42c29fc03 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons.test.tsx +++ b/packages/inspection-capture-web/test/components/HUDButtons.test.tsx @@ -3,8 +3,8 @@ import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { InteractiveStatus } from '@monkvision/types'; import { TakePictureButton, Icon } from '@monkvision/common-ui-web'; import { fireEvent, render, screen } from '@testing-library/react'; -import { PhotoCaptureHUDButtons } from '../../../src'; -import { captureButtonForegroundColors } from '../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles'; +import { HUDButtons } from '../../src/components'; +import { captureButtonForegroundColors } from '../../src/components/HUDButtons/HUDButtons.styles'; const GALLERY_BTN_TEST_ID = 'monk-gallery-btn'; const GALLERY_BADGE_TEST_ID = 'monk-gallery-badge'; @@ -16,7 +16,7 @@ describe('CaptureHUDButtons component', () => { }); it('should display 3 buttons : open gallery, take picture and close', () => { - const { unmount } = render(); + const { unmount } = render(); expect(Icon).toHaveBeenCalledTimes(2); expect(TakePictureButton).toHaveBeenCalledTimes(1); @@ -26,7 +26,7 @@ describe('CaptureHUDButtons component', () => { describe('Gallery button', () => { it('should not be disabled by default', () => { - const { unmount } = render(); + const { unmount } = render(); const galleryBtnEl = screen.getByTestId(GALLERY_BTN_TEST_ID); expect(galleryBtnEl.getAttribute('disabled')).toBeNull(); @@ -35,7 +35,7 @@ describe('CaptureHUDButtons component', () => { }); it('should be disabled when the galleryDisabled prop is true', () => { - const { unmount } = render(); + const { unmount } = render(); const galleryBtnEl = screen.getByTestId(GALLERY_BTN_TEST_ID); expect(galleryBtnEl.getAttribute('disabled')).toBeDefined(); @@ -45,7 +45,7 @@ describe('CaptureHUDButtons component', () => { it('should get passed the onOpenGallery callback', () => { const onOpenGallery = jest.fn(); - const { unmount } = render(); + const { unmount } = render(); const galleryBtnEl = screen.getByTestId(GALLERY_BTN_TEST_ID); fireEvent.click(galleryBtnEl); @@ -56,7 +56,7 @@ describe('CaptureHUDButtons component', () => { it('should display an image icon when no galleryPreview is provided', () => { const expectedIcon = 'gallery'; - const { unmount } = render(); + const { unmount } = render(); expect((Icon as jest.Mock).mock.calls).toContainEqual([ { @@ -72,7 +72,7 @@ describe('CaptureHUDButtons component', () => { it('should display background image the galleryPreview prop is provided', () => { const uri = 'test-uri'; - const { unmount } = render(); + const { unmount } = render(); const galleryBtnEl = screen.getByTestId(GALLERY_BTN_TEST_ID); const backgroundDiv = galleryBtnEl.querySelector('div'); @@ -83,7 +83,7 @@ describe('CaptureHUDButtons component', () => { }); it('should not display the notification badge if not asked to', () => { - const { unmount } = render(); + const { unmount } = render(); const galleryBadgeEl = screen.getByTestId(GALLERY_BADGE_TEST_ID); expect(galleryBadgeEl).toHaveStyle({ visibility: 'hidden' }); @@ -92,7 +92,7 @@ describe('CaptureHUDButtons component', () => { }); it('should display the notification badge if asked to', () => { - const { unmount } = render(); + const { unmount } = render(); const galleryBadgeEl = screen.getByTestId(GALLERY_BADGE_TEST_ID); expect(galleryBadgeEl).toHaveStyle({ visibility: 'visible' }); @@ -101,9 +101,7 @@ describe('CaptureHUDButtons component', () => { }); it('should display the notification badge with the number of pictures to retake', () => { - const { unmount } = render( - , - ); + const { unmount } = render(); const galleryBadgeEl = screen.getByTestId(GALLERY_BADGE_TEST_ID); expect(galleryBadgeEl.textContent).toBe('10'); @@ -116,7 +114,7 @@ describe('CaptureHUDButtons component', () => { const takePictureButtonMock = TakePictureButton as unknown as jest.Mock; it('should not be disabled by default', () => { - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(takePictureButtonMock, { disabled: false }); @@ -124,7 +122,7 @@ describe('CaptureHUDButtons component', () => { }); it('should be disabled when the takePictureDisabled prop is true', () => { - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(takePictureButtonMock, { disabled: true }); @@ -132,7 +130,7 @@ describe('CaptureHUDButtons component', () => { }); it('should have a size of 85px', () => { - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(takePictureButtonMock, { size: 85 }); @@ -141,7 +139,7 @@ describe('CaptureHUDButtons component', () => { it('should get passed the onTakePicture callback', () => { const onTakePicture = jest.fn(); - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(takePictureButtonMock, { onClick: onTakePicture }); @@ -151,7 +149,7 @@ describe('CaptureHUDButtons component', () => { describe('Close button', () => { it('should not be displayed by default', () => { - const { unmount } = render(); + const { unmount } = render(); const closeBtn = screen.getByTestId(CLOSE_BTN_TEST_ID); expect(closeBtn).toHaveStyle({ visibility: 'hidden' }); @@ -160,7 +158,7 @@ describe('CaptureHUDButtons component', () => { }); it('should displayed when showCloseButton is true', () => { - const { unmount } = render(); + const { unmount } = render(); const closeBtn = screen.getByTestId(CLOSE_BTN_TEST_ID); expect(closeBtn).toHaveStyle({ visibility: 'visible' }); @@ -169,7 +167,7 @@ describe('CaptureHUDButtons component', () => { }); it('should not be disabled by default', () => { - const { unmount } = render(); + const { unmount } = render(); const closeBtn = screen.getByTestId(CLOSE_BTN_TEST_ID); expect(closeBtn.getAttribute('disabled')).toBeNull(); @@ -178,7 +176,7 @@ describe('CaptureHUDButtons component', () => { }); it('should be disabled when the closeDisabled prop is true', () => { - const { unmount } = render(); + const { unmount } = render(); const closeBtn = screen.getByTestId(CLOSE_BTN_TEST_ID); expect(closeBtn.getAttribute('disabled')).toBeDefined(); @@ -188,7 +186,7 @@ describe('CaptureHUDButtons component', () => { it('should get passed the onClose callback', () => { const onClose = jest.fn(); - const { unmount } = render(); + const { unmount } = render(); const closeBtn = screen.getByTestId(CLOSE_BTN_TEST_ID); fireEvent.click(closeBtn); @@ -199,7 +197,7 @@ describe('CaptureHUDButtons component', () => { it('should display an image icon', () => { const expectedIcon = 'close'; - const { unmount } = render(); + const { unmount } = render(); expect((Icon as jest.Mock).mock.calls).toContainEqual([ { diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay.test.tsx b/packages/inspection-capture-web/test/components/HUDOverlay.test.tsx similarity index 84% rename from packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay.test.tsx rename to packages/inspection-capture-web/test/components/HUDOverlay.test.tsx index fc6473d47..8c5edd42b 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDOverlay.test.tsx +++ b/packages/inspection-capture-web/test/components/HUDOverlay.test.tsx @@ -11,12 +11,12 @@ import { useTranslation } from 'react-i18next'; import { useObjectTranslation } from '@monkvision/common'; import { MonkNetworkError } from '@monkvision/network'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { PhotoCaptureHUDOverlay, PhotoCaptureHUDOverlayProps } from '../../../src'; -import { PhotoCaptureErrorName } from '../../../src/PhotoCapture/errors'; +import { HUDOverlay, HUDOverlayProps } from '../../src/components'; +import { PhotoCaptureErrorName } from '../../src/errors'; const OVERLAY_TEST_ID = 'overlay'; -function createProps(): PhotoCaptureHUDOverlayProps { +function createProps(): HUDOverlayProps { return { isCaptureLoading: false, captureError: null, @@ -40,14 +40,14 @@ function mockTranslationFunction(returnValue: string, mockTObj = false): jest.Mo return mock; } -describe('PhotoCaptureHUDOverlay component', () => { +describe('HUDOverlay component', () => { afterEach(() => { jest.clearAllMocks(); }); it('should return null there is no loading or error', () => { const props = createProps(); - const { container, unmount } = render(); + const { container, unmount } = render(); expect(container).toBeEmptyDOMElement(); @@ -57,7 +57,7 @@ describe('PhotoCaptureHUDOverlay component', () => { it('should display a fixed overlay on top of the screen', () => { const props = createProps(); props.isCaptureLoading = true; - const { unmount } = render(); + const { unmount } = render(); expect(screen.getByTestId(OVERLAY_TEST_ID)).toHaveStyle({ position: 'absolute', @@ -75,7 +75,7 @@ describe('PhotoCaptureHUDOverlay component', () => { it('should NOT display a spinner on the screen if the camera is loading', () => { const props = createProps(); props.handle.isLoading = true; - const { unmount } = render(); + const { unmount } = render(); expect(Spinner).not.toHaveBeenCalled(); @@ -85,7 +85,7 @@ describe('PhotoCaptureHUDOverlay component', () => { it('should display a spinner on the screen if the capture is loading', () => { const props = createProps(); props.isCaptureLoading = true; - const { unmount } = render(); + const { unmount } = render(); expect(Spinner).toHaveBeenCalled(); @@ -110,14 +110,14 @@ describe('PhotoCaptureHUDOverlay component', () => { ].forEach(({ errors, label }) => { it(`should display the proper error label for ${errors ?? 'unknown capture'} errors`, () => { const props = createProps(); - const { unmount, rerender } = render(); + const { unmount, rerender } = render(); errors.forEach((err) => { const translationLabel = `test-${err}`; const tMock = mockTranslationFunction(translationLabel); props.captureError = new Error(); (props.captureError as Error).name = err ?? 'unknown'; - rerender(); + rerender(); expect(tMock).toHaveBeenCalledWith(label); expect( @@ -137,7 +137,7 @@ describe('PhotoCaptureHUDOverlay component', () => { const tObjMock = mockTranslationFunction(label, true); const props = createProps(); props.handle.error = { type: UserMediaErrorType.OTHER } as UserMediaError; - const { unmount } = render(); + const { unmount } = render(); expect(getCameraErrorLabel).toHaveBeenCalledWith(props.handle.error.type); expect(tObjMock).toHaveBeenCalledWith(obj); @@ -150,7 +150,7 @@ describe('PhotoCaptureHUDOverlay component', () => { it('should display a retry button for camera errors', () => { const props = createProps(); props.handle.error = { type: UserMediaErrorType.OTHER } as UserMediaError; - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(Button, { onClick: props.handle.retry, @@ -170,7 +170,7 @@ describe('PhotoCaptureHUDOverlay component', () => { const props = createProps(); props.captureError = new Error(); (props.captureError as Error).name = error; - const { unmount } = render(); + const { unmount } = render(); expect(Button).not.toHaveBeenCalled(); @@ -182,7 +182,7 @@ describe('PhotoCaptureHUDOverlay component', () => { const props = createProps(); props.captureError = new Error(); (props.captureError as Error).name = 'unknown'; - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(Button, { onClick: props.onRetry, diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot.test.tsx b/packages/inspection-capture-web/test/components/ZoomOutShot.test.tsx similarity index 53% rename from packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot.test.tsx rename to packages/inspection-capture-web/test/components/ZoomOutShot.test.tsx index 8b24952be..a7e456ca7 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddDamage1stShot.test.tsx +++ b/packages/inspection-capture-web/test/components/ZoomOutShot.test.tsx @@ -1,48 +1,44 @@ -jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter', () => ({ - PhotoCaptureHUDCounter: jest.fn(() => <>), +jest.mock('../../src/components/Counter', () => ({ + Counter: jest.fn(() => <>), })); -jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCancelButton', () => ({ - PhotoCaptureHUDCancelButton: jest.fn(() => <>), +jest.mock('../../src/components/CancelButton', () => ({ + CancelButton: jest.fn(() => <>), })); import { act, render, screen } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { Button, DynamicSVG } from '@monkvision/common-ui-web'; import { useTranslation } from 'react-i18next'; -import { - PhotoCaptureHUDCancelButton, - PhotoCaptureHUDCounter, - PhotoCaptureHUDElementsAddDamage1stShot, -} from '../../../src'; -import { crosshairSvg } from '../../../src/assets'; -import { PhotoCaptureMode } from '../../../src/PhotoCapture/hooks'; - -describe('PhotoCaptureHUDElementsAddDamage1stShot component', () => { +import { CancelButton, Counter, ZoomOutShot } from '../../src/components'; +import { crosshairSvg } from '../../src/assets'; +import { CaptureMode } from '../../src/types'; + +describe('ZoomOutShot component', () => { afterEach(() => { jest.clearAllMocks(); }); it('should display a crosshair SVG on the screen', () => { - const { unmount } = render(); + const { unmount } = render(); expectPropsOnChildMock(DynamicSVG, { svg: crosshairSvg }); unmount(); }); - it('should display the PhotoCaptureHUDCounter component on AD 1st Shot mode', () => { - const { unmount } = render(); + it('should display the Counter component on AD 1st Shot mode', () => { + const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDCounter, { mode: PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT }); + expectPropsOnChildMock(Counter, { mode: CaptureMode.ADD_DAMAGE_1ST_SHOT }); unmount(); }); it('should display the a cancel button component and pass it the onCancel prop', () => { const onCancel = jest.fn(); - const { unmount } = render(); + const { unmount } = render(); - expectPropsOnChildMock(PhotoCaptureHUDCancelButton, { onCancel }); + expectPropsOnChildMock(CancelButton, { onCancel }); unmount(); }); @@ -51,7 +47,7 @@ describe('PhotoCaptureHUDElementsAddDamage1stShot component', () => { const label = 'test-label'; const tMock = jest.fn(() => label); (useTranslation as jest.Mock).mockImplementationOnce(() => ({ t: tMock })); - const { unmount } = render(); + const { unmount } = render(); expect(tMock).toHaveBeenCalledWith('photo.hud.addDamage.infoBtn'); expectPropsOnChildMock(Button, { children: label }); @@ -62,7 +58,7 @@ describe('PhotoCaptureHUDElementsAddDamage1stShot component', () => { it('should remove the info popup when the user clicks on it', () => { const testId = 'button-test-id'; (Button as unknown as jest.Mock).mockImplementation(() =>
); - const { unmount } = render(); + const { unmount } = render(); expect(screen.queryByTestId(testId)).not.toBeNull(); act(() => { diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useAdaptiveCameraConfig.test.ts b/packages/inspection-capture-web/test/hooks/useAdaptiveCameraConfig.test.ts similarity index 97% rename from packages/inspection-capture-web/test/PhotoCapture/hooks/useAdaptiveCameraConfig.test.ts rename to packages/inspection-capture-web/test/hooks/useAdaptiveCameraConfig.test.ts index 43720db9f..957daa203 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useAdaptiveCameraConfig.test.ts +++ b/packages/inspection-capture-web/test/hooks/useAdaptiveCameraConfig.test.ts @@ -1,8 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import { - useAdaptiveCameraConfig, - UseAdaptiveCameraConfigOptions, -} from '../../../src/PhotoCapture/hooks'; +import { useAdaptiveCameraConfig, UseAdaptiveCameraConfigOptions } from '../../src/hooks'; import { CameraResolution, CompressionFormat } from '@monkvision/types'; import { act } from '@testing-library/react'; diff --git a/packages/inspection-capture-web/test/hooks/useAddDamageMode.test.ts b/packages/inspection-capture-web/test/hooks/useAddDamageMode.test.ts new file mode 100644 index 000000000..10d8fec12 --- /dev/null +++ b/packages/inspection-capture-web/test/hooks/useAddDamageMode.test.ts @@ -0,0 +1,91 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useAddDamageMode } from '../../src/hooks'; +import { CaptureMode } from '../../src/types'; +import { act } from '@testing-library/react'; +import { AddDamage } from '@monkvision/types'; + +describe('useAddDamageMode hook', () => { + it('should be in the SIGHT mode by default if damageDisclosure is false', () => { + const { result, unmount } = renderHook(useAddDamageMode, { + initialProps: { + addDamage: AddDamage.PART_SELECT, + handleOpenGallery: jest.fn(), + damageDisclosure: false, + }, + }); + expect(result.current.mode).toEqual(CaptureMode.SIGHT); + unmount(); + }); + + it('should be in the PART SELECT mode by default if damageDisclosure is true', () => { + const { result, unmount } = renderHook(useAddDamageMode, { + initialProps: { + addDamage: AddDamage.PART_SELECT, + handleOpenGallery: jest.fn(), + damageDisclosure: true, + }, + }); + expect(result.current.mode).toEqual(CaptureMode.ADD_DAMAGE_PART_SELECT); + unmount(); + }); + + it('should switch to ADD_DAMAGE_1ST_SHOT', () => { + const { result, unmount } = renderHook(useAddDamageMode, { + initialProps: { + addDamage: AddDamage.TWO_SHOT, + handleOpenGallery: jest.fn(), + damageDisclosure: false, + }, + }); + act(() => result.current.handleAddDamage()); + expect(result.current.mode).toEqual(CaptureMode.ADD_DAMAGE_1ST_SHOT); + unmount(); + }); + + it('should switch to ADD_DAMAGE_2ND_SHOT', () => { + const { result, unmount } = renderHook(useAddDamageMode, { + initialProps: { + addDamage: AddDamage.TWO_SHOT, + handleOpenGallery: jest.fn(), + damageDisclosure: false, + }, + }); + act(() => result.current.handleAddDamage()); + act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); + expect(result.current.mode).toEqual(CaptureMode.ADD_DAMAGE_2ND_SHOT); + unmount(); + }); + + it('should go back to the SIGHT mode', () => { + const { result, unmount } = renderHook(useAddDamageMode, { + initialProps: { + addDamage: AddDamage.TWO_SHOT, + handleOpenGallery: jest.fn(), + damageDisclosure: false, + }, + }); + act(() => result.current.handleAddDamage()); + act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); + act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); + expect(result.current.mode).toEqual(CaptureMode.SIGHT); + unmount(); + }); + + it('should allow to cancel add damage at any time', () => { + const { result, unmount } = renderHook(useAddDamageMode, { + initialProps: { + addDamage: AddDamage.TWO_SHOT, + handleOpenGallery: jest.fn(), + damageDisclosure: false, + }, + }); + act(() => result.current.handleAddDamage()); + act(() => result.current.handleCancelAddDamage()); + expect(result.current.mode).toEqual(CaptureMode.SIGHT); + act(() => result.current.handleAddDamage()); + act(() => result.current.updatePhotoCaptureModeAfterPictureTaken()); + act(() => result.current.handleCancelAddDamage()); + expect(result.current.mode).toEqual(CaptureMode.SIGHT); + unmount(); + }); +}); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useBadConnectionWarning.test.tsx b/packages/inspection-capture-web/test/hooks/useBadConnectionWarning.test.tsx similarity index 98% rename from packages/inspection-capture-web/test/PhotoCapture/hooks/useBadConnectionWarning.test.tsx rename to packages/inspection-capture-web/test/hooks/useBadConnectionWarning.test.tsx index 2eebf935c..aa0c2d121 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useBadConnectionWarning.test.tsx +++ b/packages/inspection-capture-web/test/hooks/useBadConnectionWarning.test.tsx @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import { useBadConnectionWarning } from '../../../src/PhotoCapture/hooks'; +import { useBadConnectionWarning } from '../../src/hooks'; import { act } from '@testing-library/react'; import { createFakePromise } from '@monkvision/test-utils'; diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts b/packages/inspection-capture-web/test/hooks/usePhotoCaptureImages.test.ts similarity index 93% rename from packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts rename to packages/inspection-capture-web/test/hooks/usePhotoCaptureImages.test.ts index 6542682b7..41aa2250a 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts +++ b/packages/inspection-capture-web/test/hooks/usePhotoCaptureImages.test.ts @@ -6,7 +6,7 @@ jest.mock('@monkvision/common', () => ({ getInspectionImages: jest.fn(() => inspectionImagesMock), })); -import { usePhotoCaptureImages } from '../../../src/PhotoCapture/hooks'; +import { usePhotoCaptureImages } from '../../src/hooks'; import { renderHook } from '@testing-library/react-hooks'; import { getInspectionImages, useMonkState } from '@monkvision/common'; @@ -20,7 +20,12 @@ describe('usePhotoCaptureImages hook', () => { const { result, unmount } = renderHook(usePhotoCaptureImages, { initialProps: inspectionId }); expect(useMonkState).toHaveBeenCalled(); - expect(getInspectionImages).toHaveBeenCalledWith(inspectionId, stateMock.images, true); + expect(getInspectionImages).toHaveBeenCalledWith( + inspectionId, + stateMock.images, + undefined, + true, + ); expect(result.current).toEqual(inspectionImagesMock); unmount(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePictureTaken.test.ts b/packages/inspection-capture-web/test/hooks/usePictureTaken.test.ts similarity index 79% rename from packages/inspection-capture-web/test/PhotoCapture/hooks/usePictureTaken.test.ts rename to packages/inspection-capture-web/test/hooks/usePictureTaken.test.ts index 9f673d0cd..df36e59a7 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePictureTaken.test.ts +++ b/packages/inspection-capture-web/test/hooks/usePictureTaken.test.ts @@ -1,21 +1,18 @@ import { TaskName, MonkPicture } from '@monkvision/types'; -import { - AddDamageHandle, - PhotoCaptureMode, - usePictureTaken, - UseTakePictureParams, -} from '../../../src/PhotoCapture/hooks'; +import { AddDamageHandle, usePictureTaken, UseTakePictureParams } from '../../src/hooks'; +import { CaptureMode } from '../../src/types'; import { renderHook } from '@testing-library/react-hooks'; +import { PhotoCaptureSightState } from '../../src/PhotoCapture/hooks'; function createParams(): UseTakePictureParams { return { - sightState: { + captureState: { setLastPictureTakenUri: jest.fn(), takeSelectedSight: jest.fn(), selectedSight: { id: 'test-selected-sight', tasks: [TaskName.WHEEL_ANALYSIS] }, }, addDamageHandle: { - mode: PhotoCaptureMode.SIGHT, + mode: CaptureMode.SIGHT, updatePhotoCaptureModeAfterPictureTaken: jest.fn(), }, uploadQueue: { @@ -44,10 +41,10 @@ describe('usePictureTaken hook', () => { const initialProps = createParams(); const { result, unmount } = renderHook(usePictureTaken, { initialProps }); - expect(initialProps.sightState.setLastPictureTakenUri).not.toHaveBeenCalled(); + expect(initialProps.captureState.setLastPictureTakenUri).not.toHaveBeenCalled(); const picture = createMonkPicture(); result.current(picture); - expect(initialProps.sightState.setLastPictureTakenUri).toHaveBeenCalledWith(picture.uri); + expect(initialProps.captureState.setLastPictureTakenUri).toHaveBeenCalledWith(picture.uri); unmount(); }); @@ -62,8 +59,8 @@ describe('usePictureTaken hook', () => { expect(initialProps.uploadQueue.push).toHaveBeenCalledWith({ mode: initialProps.addDamageHandle.mode, picture, - sightId: initialProps.sightState.selectedSight.id, - tasks: initialProps.sightState.selectedSight.tasks, + sightId: (initialProps.captureState as PhotoCaptureSightState).selectedSight.id, + tasks: (initialProps.captureState as PhotoCaptureSightState).selectedSight.tasks, }); unmount(); @@ -74,7 +71,7 @@ describe('usePictureTaken hook', () => { const initialProps = { ...createParams(), tasksBySight: { - [createParams().sightState.selectedSight.id]: tasks, + [(createParams().captureState as PhotoCaptureSightState).selectedSight.id]: tasks, }, }; const { result, unmount } = renderHook(usePictureTaken, { initialProps }); @@ -93,7 +90,7 @@ describe('usePictureTaken hook', () => { ...defaultParams, addDamageHandle: { ...defaultParams.addDamageHandle, - mode: PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT, + mode: CaptureMode.ADD_DAMAGE_2ND_SHOT, } as unknown as AddDamageHandle, }; const { result, unmount } = renderHook(usePictureTaken, { initialProps }); @@ -113,9 +110,13 @@ describe('usePictureTaken hook', () => { const initialProps = createParams(); const { result, unmount } = renderHook(usePictureTaken, { initialProps }); - expect(initialProps.sightState.takeSelectedSight).not.toHaveBeenCalled(); + expect( + (initialProps.captureState as PhotoCaptureSightState).takeSelectedSight, + ).not.toHaveBeenCalled(); result.current(createMonkPicture()); - expect(initialProps.sightState.takeSelectedSight).toHaveBeenCalled(); + expect( + (initialProps.captureState as PhotoCaptureSightState).takeSelectedSight, + ).toHaveBeenCalled(); unmount(); }); @@ -126,13 +127,15 @@ describe('usePictureTaken hook', () => { ...defaultParams, addDamageHandle: { ...defaultParams.addDamageHandle, - mode: PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT, + mode: CaptureMode.ADD_DAMAGE_2ND_SHOT, } as unknown as AddDamageHandle, }; const { result, unmount } = renderHook(usePictureTaken, { initialProps }); result.current(createMonkPicture()); - expect(initialProps.sightState.takeSelectedSight).not.toHaveBeenCalled(); + expect( + (initialProps.captureState as PhotoCaptureSightState).takeSelectedSight, + ).not.toHaveBeenCalled(); unmount(); }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useTracking.test.ts b/packages/inspection-capture-web/test/hooks/useTracking.test.ts similarity index 96% rename from packages/inspection-capture-web/test/PhotoCapture/hooks/useTracking.test.ts rename to packages/inspection-capture-web/test/hooks/useTracking.test.ts index 706f005c4..690639dcd 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useTracking.test.ts +++ b/packages/inspection-capture-web/test/hooks/useTracking.test.ts @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import { useTracking, TrackingParams } from '../../../src/PhotoCapture/hooks'; +import { useTracking, TrackingParams } from '../../src/hooks'; import { useAnalytics } from '@monkvision/analytics'; import { useMonitoring } from '@monkvision/monitoring'; import { decodeMonkJwt } from '@monkvision/network'; diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts b/packages/inspection-capture-web/test/hooks/useUploadQueue.test.ts similarity index 97% rename from packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts rename to packages/inspection-capture-web/test/hooks/useUploadQueue.test.ts index dd4b91421..68cc81c80 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts +++ b/packages/inspection-capture-web/test/hooks/useUploadQueue.test.ts @@ -3,10 +3,10 @@ import { renderHook } from '@testing-library/react-hooks'; import { AddDamage1stShotPictureUpload, AddDamage2ndShotPictureUpload, - PhotoCaptureMode, UploadQueueParams, useUploadQueue, -} from '../../../src/PhotoCapture/hooks'; +} from '../../src/hooks'; +import { CaptureMode } from '../../src/types'; import { ComplianceIssue, TaskName } from '@monkvision/types'; import { ImageUploadType, useMonkApi } from '@monkvision/network'; import { useMonitoring } from '@monkvision/monitoring'; @@ -41,7 +41,7 @@ function createParams(): UploadQueueParams { } const defaultUploadOptions = { - mode: PhotoCaptureMode.SIGHT, + mode: CaptureMode.SIGHT, picture: { uri: 'test-monk-uri', mimetype: 'test-mimetype', @@ -132,7 +132,7 @@ describe('useUploadQueue hook', () => { const process = (useQueue as jest.Mock).mock.calls[0][0]; const upload1: AddDamage1stShotPictureUpload = { - mode: PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT, + mode: CaptureMode.ADD_DAMAGE_1ST_SHOT, picture: { blob: { size: 42 } as Blob, uri: 'test-monk-uri-1', @@ -157,7 +157,7 @@ describe('useUploadQueue hook', () => { addImageMock.mockClear(); const upload2: AddDamage2ndShotPictureUpload = { - mode: PhotoCaptureMode.ADD_DAMAGE_2ND_SHOT, + mode: CaptureMode.ADD_DAMAGE_2ND_SHOT, picture: { blob: { size: 42 } as Blob, uri: 'test-monk-uri-2', @@ -181,7 +181,7 @@ describe('useUploadQueue hook', () => { addImageMock.mockClear(); await act(async () => { await process({ - mode: PhotoCaptureMode.ADD_DAMAGE_1ST_SHOT, + mode: CaptureMode.ADD_DAMAGE_1ST_SHOT, picture: { uri: 'test-monk-uri-3', mimetype: 'test-mimetype-3', diff --git a/packages/network/src/api/image/requests.ts b/packages/network/src/api/image/requests.ts index ea2c4d3f8..ab91e45bd 100644 --- a/packages/network/src/api/image/requests.ts +++ b/packages/network/src/api/image/requests.ts @@ -1,6 +1,11 @@ import ky from 'ky'; import { Dispatch } from 'react'; -import { getFileExtensions, MonkActionType, MonkCreatedOneImageAction } from '@monkvision/common'; +import { + getFileExtensions, + MonkActionType, + MonkCreatedOneImageAction, + vehiclePartLabels, +} from '@monkvision/common'; import { ComplianceOptions, Image, @@ -12,11 +17,12 @@ import { MonkPicture, TaskName, TranslationObject, + VehiclePart, } from '@monkvision/types'; import { v4 } from 'uuid'; import { labels, sights } from '@monkvision/sights'; import { getDefaultOptions, MonkApiConfig } from '../config'; -import { ApiImage, ApiImagePost, ApiImagePostTask } from '../models'; +import { ApiCenterOnElement, ApiImage, ApiImagePost, ApiImagePostTask } from '../models'; import { MonkApiResponse } from '../types'; import { mapApiImage } from './mappers'; @@ -33,6 +39,11 @@ export enum ImageUploadType { * add damage workflow. */ CLOSE_UP_2_SHOT = 'close_up_2_shot', + /** + * Upload type corresponding to a close-up picture (add-damage) in the PhotoCapture process, when using the + * part-select-shot add damage workflow. + */ + PART_SELECT_SHOT = 'part_select_shot', /** * Upload type corresponding to a video frame in the VideoCapture process. */ @@ -114,6 +125,36 @@ export interface Add2ShotCloseUpImageOptions { compliance?: ComplianceOptions; } +/** + * Options specified when adding a close up (an "add damage" image) to an inspection using the part select process. + */ +export interface AddPartSelectCloseUpImageOptions { + /** + * The type of the image upload : `ImageUploadType.PART_SELECT_SHOT`; + */ + uploadType: ImageUploadType.PART_SELECT_SHOT; + /** + * The picture to add to the inspection. + */ + picture: MonkPicture; + /** + * The ID of the inspection to add the image to. + */ + inspectionId: string; + /** + * List of damage parts chosen by User with part selected wireframe + */ + vehicleParts: VehiclePart[]; + /** + * Boolean indicating if a thumbnail request will be sent when addImage is called. + */ + useThumbnailCaching?: boolean; + /** + * Additional options used to configure the compliance locally. + */ + compliance?: ComplianceOptions; +} + /** * Options specififed when adding a video frame to a VideoCapture inspection. */ @@ -165,7 +206,8 @@ export type AddImageOptions = | AddBeautyShotImageOptions | Add2ShotCloseUpImageOptions | AddVideoFrameOptions - | AddVideoManualPhotoOptions; + | AddVideoManualPhotoOptions + | AddPartSelectCloseUpImageOptions; interface AddImageData { filename: string; @@ -173,7 +215,9 @@ interface AddImageData { } function getImageType(options: AddImageOptions): ImageType { - if (options.uploadType === ImageUploadType.CLOSE_UP_2_SHOT) { + if ( + [ImageUploadType.CLOSE_UP_2_SHOT, ImageUploadType.PART_SELECT_SHOT].includes(options.uploadType) + ) { return ImageType.CLOSE_UP; } return ImageType.BEAUTY_SHOT; @@ -199,6 +243,15 @@ function getImageLabel(options: AddImageOptions): TranslationObject | undefined nl: `Foto-handleiding Video`, }; } + if (options.uploadType === ImageUploadType.PART_SELECT_SHOT) { + const partsTranslation = options.vehicleParts.map((part) => vehiclePartLabels[part]); + return { + en: `Close Up on ${partsTranslation.map((part) => part.en).join(', ')}`, + fr: `Photo Zoomée sur ${partsTranslation.map((part) => part.en).join(', ')}`, + de: `Gezoomtes an ${partsTranslation.map((part) => part.en).join(', ')}`, + nl: `Nabij aan ${partsTranslation.map((part) => part.en).join(', ')}`, + }; + } return { en: options.firstShot ? 'Close Up (part)' : 'Close Up (damage)', fr: options.firstShot ? 'Photo Zoomée (partie)' : 'Photo Zoomée (dégât)', @@ -259,7 +312,7 @@ function createBeautyShotImageData( return { filename, body }; } -function createCloseUpImageData( +function create2ShotCloseUpImageData( options: Add2ShotCloseUpImageOptions, filetype: string, ): AddImageData { @@ -288,6 +341,35 @@ function createCloseUpImageData( return { filename, body }; } +function createPartSelectCloseUpImageData( + options: AddPartSelectCloseUpImageOptions, + filetype: string, +): AddImageData { + const filename = `part-select-${options.inspectionId}-${Date.now()}.${filetype}`; + + const body: ApiImagePost = { + acquisition: { + strategy: 'upload_multipart_form_keys', + file_key: MULTIPART_KEY_IMAGE, + }, + image_type: ImageType.CLOSE_UP, + tasks: [ + TaskName.DAMAGE_DETECTION, + { + name: TaskName.COMPLIANCES, + wait_for_result: + options.compliance?.enableCompliance && options.compliance?.useLiveCompliance, + }, + ], + detailed_viewpoint: { + centers_on: options.vehicleParts as ApiCenterOnElement[], + }, + additional_data: getAdditionalData(options), + }; + + return { filename, body }; +} + function createVideoFrameData(options: AddVideoFrameOptions, filetype: string): AddImageData { const filename = `video-frame-${options.frameIndex}.${filetype}`; @@ -326,7 +408,9 @@ function getAddImageData(options: AddImageOptions, filetype: string): AddImageDa case ImageUploadType.BEAUTY_SHOT: return createBeautyShotImageData(options, filetype); case ImageUploadType.CLOSE_UP_2_SHOT: - return createCloseUpImageData(options, filetype); + return create2ShotCloseUpImageData(options, filetype); + case ImageUploadType.PART_SELECT_SHOT: + return createPartSelectCloseUpImageData(options, filetype); case ImageUploadType.VIDEO_FRAME: return createVideoFrameData(options, filetype); case ImageUploadType.VIDEO_MANUAL_PHOTO: diff --git a/packages/network/src/api/models/image.ts b/packages/network/src/api/models/image.ts index 06da6b64f..00633e1f7 100644 --- a/packages/network/src/api/models/image.ts +++ b/packages/network/src/api/models/image.ts @@ -89,5 +89,6 @@ export interface ApiImagePost { image_subtype?: ApiImageSubType; image_sibling_key?: string; compliances?: ApiCompliance; + detailed_viewpoint?: ApiViewpointComponent; additional_data?: ApiImageAdditionalData; } diff --git a/packages/network/test/api/image/requests.test.ts b/packages/network/test/api/image/requests.test.ts index 00d853622..6ef809a16 100644 --- a/packages/network/test/api/image/requests.test.ts +++ b/packages/network/test/api/image/requests.test.ts @@ -11,13 +11,21 @@ jest.mock('../../../src/api/image/mappers', () => ({ import { labels, sights } from '@monkvision/sights'; import ky from 'ky'; -import { ComplianceIssue, ImageStatus, ImageSubtype, ImageType, TaskName } from '@monkvision/types'; -import { getFileExtensions, MonkActionType } from '@monkvision/common'; +import { + ComplianceIssue, + ImageStatus, + ImageSubtype, + ImageType, + TaskName, + VehiclePart, +} from '@monkvision/types'; +import { getFileExtensions, MonkActionType, vehiclePartLabels } from '@monkvision/common'; import { getDefaultOptions } from '../../../src/api/config'; import { Add2ShotCloseUpImageOptions, AddBeautyShotImageOptions, addImage, + AddPartSelectCloseUpImageOptions, AddVideoFrameOptions, AddVideoManualPhotoOptions, ImageUploadType, @@ -51,7 +59,7 @@ function createBeautyShotImageOptions(): AddBeautyShotImageOptions { }; } -function createCloseUpImageOptions(): Add2ShotCloseUpImageOptions { +function create2ShotCloseUpImageOptions(): Add2ShotCloseUpImageOptions { return { uploadType: ImageUploadType.CLOSE_UP_2_SHOT, picture: { @@ -72,6 +80,26 @@ function createCloseUpImageOptions(): Add2ShotCloseUpImageOptions { }; } +function createPartSelectCloseUpImageOptions(): AddPartSelectCloseUpImageOptions { + return { + uploadType: ImageUploadType.PART_SELECT_SHOT, + picture: { + blob: { size: 424 } as Blob, + uri: 'test-uri', + height: 720, + width: 1280, + mimetype: 'image/jpeg', + }, + inspectionId: 'test-inspection-id', + vehicleParts: [VehiclePart.HEAD_LIGHT_RIGHT], + compliance: { + enableCompliance: true, + complianceIssues: [ComplianceIssue.INTERIOR_NOT_SUPPORTED], + }, + useThumbnailCaching: true, + }; +} + function createVideoFrameOptions(): AddVideoFrameOptions { return { uploadType: ImageUploadType.VIDEO_FRAME, @@ -291,8 +319,8 @@ describe('Image requests', () => { ); }); - it('should properly create the formdata for a closeup', async () => { - const options = createCloseUpImageOptions(); + it('should properly create the formdata for a 2Shot closeup', async () => { + const options = create2ShotCloseUpImageOptions(); await addImage(options, apiConfig); expect(ky.post).toHaveBeenCalled(); @@ -329,6 +357,45 @@ describe('Image requests', () => { ); }); + it('should properly create the formdata for a part select closeup', async () => { + const options = createPartSelectCloseUpImageOptions(); + await addImage(options, apiConfig); + + expect(ky.post).toHaveBeenCalled(); + const formData = (ky.post as jest.Mock).mock.calls[0][1].body as FormData; + expect(typeof formData?.get('json')).toBe('string'); + + const partsTranslation = options.vehicleParts.map((part) => vehiclePartLabels[part]); + expect(JSON.parse(formData.get('json') as string)).toEqual({ + acquisition: { + strategy: 'upload_multipart_form_keys', + file_key: 'image', + }, + image_type: ImageType.CLOSE_UP, + tasks: [TaskName.DAMAGE_DETECTION, { name: TaskName.COMPLIANCES }], + additional_data: { + label: { + en: `Close Up on ${partsTranslation.map((part) => part.en).join(', ')}`, + fr: `Photo Zoomée sur ${partsTranslation.map((part) => part.en).join(', ')}`, + de: `Gezoomtes an ${partsTranslation.map((part) => part.en).join(', ')}`, + nl: `Nabij aan ${partsTranslation.map((part) => part.en).join(', ')}`, + }, + created_at: expect.any(String), + }, + detailed_viewpoint: { + centers_on: options.vehicleParts, + }, + }); + expect(getFileExtensions).toHaveBeenCalledWith(options.picture.mimetype); + const filetype = (getFileExtensions as jest.Mock).mock.results[0].value[0]; + const prefix = 'part-select'; + expect(fileConstructorSpy).toHaveBeenCalledWith( + [options.picture.blob], + expect.stringMatching(new RegExp(`${prefix}-${options.inspectionId}-\\d{13}.${filetype}`)), + { type: filetype }, + ); + }); + it('should properly create the formdata for a video frame', async () => { const options = createVideoFrameOptions(); await addImage(options, apiConfig); diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 2867fe546..6d91f593a 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -28,15 +28,35 @@ export enum PhotoCaptureTutorialOption { */ DISABLED = 'disabled', /** - * Photo capture is enable. + * Photo capture is enabled. */ ENABLED = 'enabled', /** - * Photo capture is enable only time. + * Photo capture is enabled only time. */ FIRST_TIME_ONLY = 'first_time_only', } +/** + * Enumeration of the Add Damage options. + */ +export enum AddDamage { + /** + * Add Damage is disabled. + */ + DISABLED = 'disabled', + /** + * Add Damage is enabled with Two Shot: + * First shot for the zoom out image and second for the close up. The vehicle part will be infered. + */ + TWO_SHOT = 'two_shot', + /** + * Add Damage is enabled with Part select: + * Parts must be selected before taken a single close up shot. + */ + PART_SELECT = 'part_select', +} + /** * Configuration used to configure the Camera and picture output of the SDK. */ @@ -174,12 +194,11 @@ export type PhotoCaptureAppConfig = SharedCaptureAppConfig & */ allowSkipRetake?: boolean; /** - * Boolean indicating if `Add Damage` feature should be enabled or not. If disabled, the `Add Damage` button will - * be hidden. + * Options for Add Damage. If disabled, the `Add Damage` button will be hidden. * - * @default true + * @default AddDamage.PART_SELECT. */ - enableAddDamage?: boolean; + addDamage?: AddDamage; /** * A collection of sight guidelines in different language with a list of sightIds associate to it. */