diff --git a/formulus-formplayer/src/FinalizeRenderer.tsx b/formulus-formplayer/src/FinalizeRenderer.tsx index a44da5e97..32d208d20 100644 --- a/formulus-formplayer/src/FinalizeRenderer.tsx +++ b/formulus-formplayer/src/FinalizeRenderer.tsx @@ -1,10 +1,20 @@ -import React from 'react'; -import { Box, Button, List, ListItem, ListItemText, Typography, Paper } from '@mui/material'; +import React, { useMemo } from 'react'; +import { Box, Button, List, ListItem, ListItemText, Typography, Paper, Divider, Link } from '@mui/material'; import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; import { withJsonFormsControlProps, useJsonForms } from '@jsonforms/react'; import { ControlProps } from '@jsonforms/core'; import { ErrorObject } from 'ajv'; import { useFormContext } from './App'; +import EditIcon from '@mui/icons-material/Edit'; + +interface SummaryItem { + label: string; + value: any; + path: string; + pageIndex: number; + type?: string; + format?: string; +} const FinalizeRenderer = ({ schema, @@ -19,10 +29,186 @@ const FinalizeRenderer = ({ const { core } = useJsonForms(); const errors = core?.errors || []; const { formInitData } = useFormContext(); + const fullSchema = core?.schema; + const fullUISchema = formInitData?.uiSchema; - // Log the props to inspect their structure - console.log('JsonForms props:', { errors, data, path, cells, schema }); - + // Helper function to get field label from schema + const getFieldLabel = (fieldPath: string, fieldSchema: any): string => { + if (!fieldSchema) return fieldPath; + return fieldSchema.title || fieldSchema.description || fieldPath.split('/').pop() || fieldPath; + }; + + // Helper function to format field value based on type + const formatFieldValue = (value: any, fieldSchema: any): string => { + if (value === null || value === undefined || value === '') { + return 'Not provided'; + } + + // Handle special formats + if (fieldSchema?.format) { + switch (fieldSchema.format) { + case 'photo': + if (typeof value === 'object' && value.uri) { + return `Photo: ${value.filename || 'Captured'}`; + } + return 'Photo captured'; + case 'qrcode': + if (typeof value === 'object' && value.data) { + return `QR Code: ${value.data}`; + } + return typeof value === 'string' ? `QR Code: ${value}` : 'QR Code scanned'; + case 'signature': + if (typeof value === 'object' && value.uri) { + return 'Signature captured'; + } + return 'Signature provided'; + case 'select_file': + if (typeof value === 'object' && value.filename) { + return `File: ${value.filename}`; + } + return 'File selected'; + case 'audio': + if (typeof value === 'object' && value.filename) { + const duration = value.metadata?.duration ? ` (${Math.round(value.metadata.duration)}s)` : ''; + return `Audio: ${value.filename}${duration}`; + } + return 'Audio recorded'; + case 'gps': + if (typeof value === 'object' && value.latitude && value.longitude) { + return `Location: ${value.latitude.toFixed(6)}, ${value.longitude.toFixed(6)}`; + } + return 'GPS location captured'; + case 'video': + if (typeof value === 'object' && value.filename) { + return `Video: ${value.filename}`; + } + return 'Video captured'; + case 'date': + return new Date(value).toLocaleDateString(); + case 'date-time': + return new Date(value).toLocaleString(); + case 'time': + return value; + } + } + + // Handle arrays + if (Array.isArray(value)) { + if (value.length === 0) return 'None'; + return value.map((item, idx) => { + if (typeof item === 'object') { + return `${idx + 1}. ${JSON.stringify(item)}`; + } + return String(item); + }).join(', '); + } + + // Handle objects + if (typeof value === 'object') { + // Check if it's a nested object with properties + if (Object.keys(value).length === 0) return 'Empty'; + return JSON.stringify(value, null, 2); + } + + // Handle booleans + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No'; + } + + // Default: convert to string + return String(value); + }; + + + // Helper function to find which page/screen a field is on + const findFieldPageMemo = useMemo(() => { + return (fieldPath: string): number => { + if (!fullUISchema || !fullUISchema.elements) return -1; + + // Normalize the field path (remove #/properties/ prefix and convert / to .) + const normalizePath = (path: string) => { + return path.replace(/^#\/properties\//, '').replace(/\//g, '.'); + }; + + const fieldName = normalizePath(fieldPath); + const screens = fullUISchema.elements; + + for (let i = 0; i < screens.length; i++) { + const screen = screens[i]; + if (screen.type === 'Finalize') continue; + + if ('elements' in screen && screen.elements) { + const hasField = screen.elements.some((el: any) => { + if (el.scope) { + const scopePath = normalizePath(el.scope); + // Exact match or field is nested under scope, or scope is nested under field + return scopePath === fieldName || + fieldName.startsWith(scopePath + '.') || + scopePath.startsWith(fieldName + '.'); + } + return false; + }); + + if (hasField) return i; + } + } + + return -1; + }; + }, [fullUISchema]); + + // Extract all form fields and their values for summary + const summaryItems = useMemo((): SummaryItem[] => { + if (!fullSchema || !data || !fullSchema.properties) return []; + + const items: SummaryItem[] = []; + + const extractFields = (schemaObj: any, dataObj: any, basePath: string = '') => { + if (!schemaObj || !schemaObj.properties) return; + + Object.keys(schemaObj.properties).forEach(key => { + const fieldSchema = schemaObj.properties[key]; + const fieldPath = basePath ? `${basePath}/${key}` : key; + const fieldValue = dataObj?.[key]; + const fullPath = `#/properties/${fieldPath}`; + + // Skip if value is empty (null, undefined, empty string, empty array, empty object) + const isEmpty = + fieldValue === null || + fieldValue === undefined || + fieldValue === '' || + (Array.isArray(fieldValue) && fieldValue.length === 0) || + (typeof fieldValue === 'object' && !Array.isArray(fieldValue) && Object.keys(fieldValue).length === 0); + + if (isEmpty) { + // Only include empty fields if they are required (to show what's missing) + const isRequired = schemaObj.required?.includes(key); + if (!isRequired) return; + } + + // Handle nested objects + if (fieldSchema.type === 'object' && fieldSchema.properties && typeof fieldValue === 'object' && !Array.isArray(fieldValue)) { + extractFields(fieldSchema, fieldValue, fieldPath); + } else { + // Add to summary + const pageIndex = findFieldPageMemo(fullPath); + items.push({ + label: getFieldLabel(fullPath, fieldSchema), + value: fieldValue, + path: fullPath, + pageIndex, + type: fieldSchema.type, + format: fieldSchema.format + }); + } + }); + }; + + extractFields(fullSchema, data); + + return items; + }, [fullSchema, data, findFieldPageMemo]); + const formatErrorPath = (path: string) => { // Remove leading slash and convert to readable format return path.replace(/^\//, '').replace(/\//g, ' > '); @@ -52,6 +238,19 @@ const FinalizeRenderer = ({ window.dispatchEvent(event); }; + const handleFieldEdit = (item: SummaryItem) => { + if (item.pageIndex >= 0) { + // Navigate to the page containing this field + const navigateEvent = new CustomEvent('navigateToPage', { + detail: { page: item.pageIndex } + }); + window.dispatchEvent(navigateEvent); + } else { + // Fallback: try to navigate using the field path + handleErrorClick(item.path); + } + }; + const handleFinalize = () => { if (!formInitData) { console.error('formInitData is not available from context, cannot submit form'); @@ -65,7 +264,7 @@ const FinalizeRenderer = ({ }; return ( - + Review and Finalize @@ -98,15 +297,105 @@ const FinalizeRenderer = ({ )} - + {/* Summary Section */} + {summaryItems.length > 0 && ( + + + Form Summary + + + Review all your entered data below. Click on any field to edit it. + + + + {summaryItems.map((item, index) => ( + + + + + + {item.label} + + + {formatFieldValue(item.value, { type: item.type, format: item.format })} + + + {item.pageIndex >= 0 && ( + handleFieldEdit(item)} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 0.5, + cursor: 'pointer', + textDecoration: 'none', + color: 'primary.main', + '&:hover': { + textDecoration: 'underline' + }, + flexShrink: 0 + }} + > + + Edit + + )} + + + {index < summaryItems.length - 1 && } + + ))} + + + + )} + + + + ); };