From 17e1c1d1c5a3e63d5a717e43af5b74d8ae25c839 Mon Sep 17 00:00:00 2001 From: Bahati308 Date: Tue, 9 Dec 2025 17:55:08 +0300 Subject: [PATCH] feat(formplayer): add summary section to Finalize screen Add comprehensive summary section displaying all entered form data with edit navigation links. Includes support for all field types (photos, QR codes, signatures, files, audio, GPS, video, etc.) and responsive design. Fixes React Hook dependency warning and build script. Closes #10 --- formulus-formplayer/src/FinalizeRenderer.tsx | 319 ++++++++++++++++++- 1 file changed, 304 insertions(+), 15 deletions(-) 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 && } + + ))} + + + + )} + + + + ); };