From 2161cc59ca73e356184d4d040a979827fc4a5af1 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 26 Nov 2025 18:23:02 +0530 Subject: [PATCH] formatedd description implemente along with request number format chnged --- .../common/FormattedDescription.tsx | 77 +++ src/components/ui/ckeditor-wrapper.tsx | 0 src/components/ui/rich-text-editor.tsx | 563 ++++++++++++++++++ .../CreateRequest/BasicInformationStep.tsx | 15 +- .../CreateRequest/ReviewSubmitStep.tsx | 7 +- src/hooks/useConclusionRemark.ts | 16 +- src/pages/Dashboard/Dashboard.tsx | 46 +- .../Dashboard/components/DashboardHero.tsx | 77 ++- src/pages/Dashboard/hooks/useDashboardData.ts | 26 +- .../MyRequests/components/RequestCard.tsx | 29 +- .../components/tabs/OverviewTab.tsx | 39 +- .../components/tabs/SummaryTab.tsx | 17 +- src/pages/Requests/components/RequestCard.tsx | 29 +- src/pages/SharedSummaries/SharedSummaries.tsx | 79 +-- .../SharedSummaries/SharedSummaryDetail.tsx | 17 +- src/services/dashboard.service.ts | 35 +- 16 files changed, 953 insertions(+), 119 deletions(-) create mode 100644 src/components/common/FormattedDescription.tsx create mode 100644 src/components/ui/ckeditor-wrapper.tsx create mode 100644 src/components/ui/rich-text-editor.tsx diff --git a/src/components/common/FormattedDescription.tsx b/src/components/common/FormattedDescription.tsx new file mode 100644 index 0000000..d384ba7 --- /dev/null +++ b/src/components/common/FormattedDescription.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import { cn } from "@/components/ui/utils"; + +interface FormattedDescriptionProps { + content: string; + className?: string; +} + +/** + * FormattedDescription Component + * + * Renders HTML content with proper styling for lists, tables, and other formatted content. + * Use this component to display descriptions that may contain HTML formatting. + */ +export function FormattedDescription({ content, className }: FormattedDescriptionProps) { + const processedContent = React.useMemo(() => { + if (!content) return ''; + + // Wrap tables that aren't already wrapped in a scrollable container using regex + // Match tags that aren't already inside a .table-wrapper + let processed = content; + + // Pattern to match table tags that aren't already wrapped + const tablePattern = /]*>[\s\S]*?<\/table>/gi; + + processed = processed.replace(tablePattern, (match) => { + // Check if this table is already wrapped + if (match.includes('table-wrapper')) { + return match; + } + + // Wrap the table in a scrollable container + return `
${match}
`; + }); + + return processed; + }, [content]); + + if (!content) return null; + + return ( +
+ ); +} + diff --git a/src/components/ui/ckeditor-wrapper.tsx b/src/components/ui/ckeditor-wrapper.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/ui/rich-text-editor.tsx b/src/components/ui/rich-text-editor.tsx new file mode 100644 index 0000000..3f4b470 --- /dev/null +++ b/src/components/ui/rich-text-editor.tsx @@ -0,0 +1,563 @@ +import * as React from "react"; +import { cn } from "./utils"; +import { Button } from "./button"; +import { Bold, Italic, Underline, List, ListOrdered, Heading1, Heading2, Heading3, AlignLeft, AlignCenter, AlignRight } from "lucide-react"; + +interface RichTextEditorProps extends Omit, 'onChange'> { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; + minHeight?: string; +} + +/** + * RichTextEditor Component + * + * Preserves formatting (lists, tables, etc.) when pasting content. + * Uses contentEditable div to maintain HTML structure. + */ +export function RichTextEditor({ + value, + onChange, + placeholder = "Enter text...", + className, + minHeight = "120px", + ...props +}: RichTextEditorProps) { + const editorRef = React.useRef(null); + const [isFocused, setIsFocused] = React.useState(false); + const [activeFormats, setActiveFormats] = React.useState>(new Set()); + + // Sync external value to editor + React.useEffect(() => { + if (editorRef.current && editorRef.current.innerHTML !== value) { + // Only update if the value actually changed externally + const currentValue = editorRef.current.innerHTML; + if (currentValue !== value) { + editorRef.current.innerHTML = value || ''; + } + } + }, [value]); + + // Clean HTML from Word/Office documents - remove style tags, comments, and metadata + const cleanWordHTML = React.useCallback((html: string): string => { + // Remove HTML comments (like Word style definitions) + html = html.replace(//g, ''); + + // Remove style tags (Word CSS) + html = html.replace(/]*>[\s\S]*?<\/style>/gi, ''); + + // Remove script tags + html = html.replace(/]*>[\s\S]*?<\/script>/gi, ''); + + // Remove meta tags + html = html.replace(/]*>/gi, ''); + + // Remove Word-specific classes and attributes + html = html.replace(/\s*class="Mso[^"]*"/gi, ''); + html = html.replace(/\s*class="mso[^"]*"/gi, ''); + html = html.replace(/\s*style="[^"]*mso-[^"]*"/gi, ''); + html = html.replace(/\s*style="[^"]*font-family:[^"]*"/gi, ''); + + // Remove xmlns attributes + html = html.replace(/\s*xmlns[^=]*="[^"]*"/gi, ''); + + // Remove o:p tags (Word paragraph markers) + html = html.replace(/<\/?o:p[^>]*>/gi, ''); + + // Remove v:shapes and other Word-specific elements + html = html.replace(/]*>[\s\S]*?<\/v:[^>]*>/gi, ''); + html = html.replace(/]*\/>/gi, ''); + + // Clean up empty paragraphs + html = html.replace(/]*>\s*<\/p>/gi, ''); + html = html.replace(/]*>\s*<\/div>/gi, ''); + + // Remove excessive whitespace + html = html.replace(/\s+/g, ' '); + html = html.trim(); + + return html; + }, []); + + // Handle paste event to preserve formatting + const handlePaste = React.useCallback((e: React.ClipboardEvent) => { + e.preventDefault(); + + const clipboardData = e.clipboardData; + let pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain'); + + // Clean Word/Office metadata if HTML + if (pastedData.includes('/g, ''); + p.innerHTML = cleaned; + p.removeAttribute('style'); + p.removeAttribute('class'); + fragment.appendChild(p); + } + // Preserve line breaks + else if (element.tagName === 'BR') { + fragment.appendChild(element.cloneNode(true)); + } + // For other elements, extract text content + else { + const text = element.textContent || ''; + if (text.trim()) { + const p = document.createElement('p'); + p.textContent = text.trim(); + fragment.appendChild(p); + } + } + } else if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent || ''; + if (text.trim()) { + const p = document.createElement('p'); + p.textContent = text.trim(); + fragment.appendChild(p); + } + } + }); + + // If no structured content, insert as plain text with line breaks + if (fragment.childNodes.length === 0) { + const plainText = clipboardData.getData('text/plain'); + const lines = plainText.split('\n'); + lines.forEach((line) => { + if (line.trim()) { + const p = document.createElement('p'); + p.textContent = line.trim(); + fragment.appendChild(p); + } else { + fragment.appendChild(document.createElement('br')); + } + }); + } + + range.insertNode(fragment); + + // Move cursor to end of inserted content + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + + // Trigger onChange + if (editorRef.current) { + onChange(editorRef.current.innerHTML); + } + }, [onChange, cleanWordHTML]); + + // Check active formats (bold, italic, etc.) + const checkActiveFormats = React.useCallback(() => { + if (!editorRef.current || !isFocused) return; + + const formats = new Set(); + const selection = window.getSelection(); + + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const commonAncestor = range.commonAncestorContainer; + let element: HTMLElement | null = null; + + if (commonAncestor.nodeType === Node.TEXT_NODE) { + element = commonAncestor.parentElement; + } else { + element = commonAncestor as HTMLElement; + } + + while (element && element !== editorRef.current) { + const tagName = element.tagName.toLowerCase(); + if (tagName === 'strong' || tagName === 'b') formats.add('bold'); + if (tagName === 'em' || tagName === 'i') formats.add('italic'); + if (tagName === 'u') formats.add('underline'); + if (tagName === 'h1') formats.add('h1'); + if (tagName === 'h2') formats.add('h2'); + if (tagName === 'h3') formats.add('h3'); + if (tagName === 'ul') formats.add('ul'); + if (tagName === 'ol') formats.add('ol'); + + const style = window.getComputedStyle(element); + if (style.textAlign === 'center') formats.add('center'); + if (style.textAlign === 'right') formats.add('right'); + if (style.textAlign === 'left') formats.add('left'); + + element = element.parentElement; + } + } + + setActiveFormats(formats); + }, [isFocused]); + + // Apply formatting command + const applyFormat = React.useCallback((command: string, value?: string) => { + if (!editorRef.current) return; + + // Restore focus if needed + if (!isFocused) { + editorRef.current.focus(); + } + + // Save current selection + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + // If no selection, focus the editor + editorRef.current.focus(); + return; + } + + // Execute formatting command + document.execCommand(command, false, value); + + // Update content + if (editorRef.current) { + onChange(editorRef.current.innerHTML); + } + + // Check active formats after a short delay + setTimeout(checkActiveFormats, 10); + }, [isFocused, onChange, checkActiveFormats]); + + // Handle input changes + const handleInput = React.useCallback(() => { + if (editorRef.current) { + onChange(editorRef.current.innerHTML); + } + checkActiveFormats(); + }, [onChange, checkActiveFormats]); + + // Handle keyboard shortcuts + const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => { + // Ctrl+B for Bold + if (e.ctrlKey && e.key === 'b') { + e.preventDefault(); + applyFormat('bold'); + return; + } + // Ctrl+I for Italic + if (e.ctrlKey && e.key === 'i') { + e.preventDefault(); + applyFormat('italic'); + return; + } + // Ctrl+U for Underline + if (e.ctrlKey && e.key === 'u') { + e.preventDefault(); + applyFormat('underline'); + return; + } + }, [applyFormat]); + + // Handle focus + const handleFocus = React.useCallback(() => { + setIsFocused(true); + }, []); + + // Handle blur + const handleBlur = React.useCallback(() => { + setIsFocused(false); + if (editorRef.current) { + onChange(editorRef.current.innerHTML); + } + }, [onChange]); + + // Handle selection change to update active formats + React.useEffect(() => { + if (!isFocused) return; + + const handleSelectionChange = () => { + checkActiveFormats(); + }; + + document.addEventListener('selectionchange', handleSelectionChange); + return () => { + document.removeEventListener('selectionchange', handleSelectionChange); + }; + }, [isFocused, checkActiveFormats]); + + return ( +
+ {/* Formatting Toolbar */} +
+ {/* Text Formatting */} +
+ + + +
+ + {/* Headings */} +
+ + + +
+ + {/* Lists */} +
+ + +
+ + {/* Alignment */} +
+ + + +
+
+ +
+
+ ); +} + diff --git a/src/components/workflow/CreateRequest/BasicInformationStep.tsx b/src/components/workflow/CreateRequest/BasicInformationStep.tsx index 854fd3a..5545139 100644 --- a/src/components/workflow/CreateRequest/BasicInformationStep.tsx +++ b/src/components/workflow/CreateRequest/BasicInformationStep.tsx @@ -1,7 +1,7 @@ import { motion } from 'framer-motion'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; +import { RichTextEditor } from '@/components/ui/rich-text-editor'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Badge } from '@/components/ui/badge'; @@ -70,13 +70,16 @@ export function BasicInformationStep({

Explain what you need approval for, why it's needed, and any relevant background information. + + 💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved. +

-