Skip to content

Commit 3de2cf7

Browse files
committed
feat: add sample data editor for template preview
Add interactive sample data editor to customize preview data: - Patient, surgery, followup, and settings tabs - Real settings integration with visual indicators - Per-category and full reset functionality - Fix settings keys (hospital/unit instead of hospital_name/unit_name)
1 parent f40a4f9 commit 3de2cf7

File tree

10 files changed

+830
-89
lines changed

10 files changed

+830
-89
lines changed

src/renderer/src/components/template-builder/LivePreview.tsx

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { useMemo } from 'react'
2-
import { TemplateStructure, TemplateType } from '../../../../shared/types/template-blocks'
2+
import { ChevronDown, Database } from 'lucide-react'
3+
import { TemplateStructure } from '../../../../shared/types/template-blocks'
34
import { renderTemplate } from '../../lib/template-renderer'
4-
import { getSampleContext } from '../../lib/template-context'
5+
import { useSampleData } from './SampleDataContext'
6+
import { SampleDataEditor } from './SampleDataEditor'
57
import { Badge } from '../ui/badge'
68
import { ScrollArea } from '../ui/scroll-area'
9+
import { cn } from '../../lib/utils'
710

811
interface LivePreviewProps {
912
templateStructure: TemplateStructure
10-
templateType: TemplateType
1113
}
1214

13-
export const LivePreview = ({ templateStructure, templateType }: LivePreviewProps) => {
14-
const sampleContext = useMemo(() => getSampleContext(templateType), [templateType])
15+
export const LivePreview = ({ templateStructure }: LivePreviewProps) => {
16+
const { sampleContext, isEditorOpen, toggleEditor, hasCustomizations } = useSampleData()
1517

1618
const previewHtml = useMemo(() => {
1719
try {
@@ -23,30 +25,45 @@ export const LivePreview = ({ templateStructure, templateType }: LivePreviewProp
2325
}, [templateStructure, sampleContext])
2426

2527
return (
26-
<div className="flex flex-col h-full">
28+
<div className="flex flex-col h-full overflow-hidden">
2729
{/* Preview Header */}
28-
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/30">
30+
<div className="flex-shrink-0 flex items-center justify-between px-4 py-2 border-b bg-muted/30">
2931
<span className="text-sm font-medium">Live Preview</span>
30-
<Badge variant="secondary" className="text-xs">
32+
<Badge
33+
variant="secondary"
34+
className={cn(
35+
'text-xs cursor-pointer hover:bg-secondary/80 transition-colors select-none',
36+
hasCustomizations && 'ring-1 ring-primary/30'
37+
)}
38+
onClick={toggleEditor}
39+
>
40+
<Database className="h-3 w-3 mr-1" />
3141
Sample Data
42+
<ChevronDown
43+
className={cn('h-3 w-3 ml-1 transition-transform', isEditorOpen && 'rotate-180')}
44+
/>
3245
</Badge>
3346
</div>
3447

35-
{/* Preview Content */}
36-
<ScrollArea className="flex-1">
37-
<div className="p-6 bg-muted/30 min-h-full">
38-
<div className="flex justify-center">
39-
{/* A4 Paper Simulation */}
40-
<div
41-
className="bg-white rounded shadow-xl border"
42-
style={{
43-
width: '210mm',
44-
minHeight: '297mm',
45-
maxWidth: '100%',
46-
boxShadow:
47-
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.05)'
48-
}}
49-
>
48+
{/* Collapsible editor */}
49+
{isEditorOpen && <SampleDataEditor />}
50+
51+
{/* Preview Content - scrollable within container */}
52+
<div className="flex-1 min-h-0 overflow-hidden">
53+
<ScrollArea className="h-full">
54+
<div className="p-6 bg-muted/30">
55+
<div className="flex justify-center">
56+
{/* A4 Paper Simulation */}
57+
<div
58+
className="bg-white rounded shadow-xl border"
59+
style={{
60+
width: '210mm',
61+
minHeight: '297mm',
62+
maxWidth: '100%',
63+
boxShadow:
64+
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.05)'
65+
}}
66+
>
5067
{/* Print styles scoped to preview */}
5168
<style>
5269
{`
@@ -175,10 +192,11 @@ export const LivePreview = ({ templateStructure, templateType }: LivePreviewProp
175192
className="preview-content"
176193
dangerouslySetInnerHTML={{ __html: previewHtml }}
177194
/>
195+
</div>
178196
</div>
179197
</div>
180-
</div>
181-
</ScrollArea>
198+
</ScrollArea>
199+
</div>
182200
</div>
183201
)
184202
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import {
2+
createContext,
3+
useContext,
4+
useState,
5+
useMemo,
6+
useCallback,
7+
type ReactNode
8+
} from 'react'
9+
import { TemplateContext, TemplateType } from '../../../../shared/types/template-blocks'
10+
import { useSettings } from '../../contexts/SettingsContext'
11+
import { getSampleContext } from '../../lib/template-context'
12+
13+
type TabId = 'patient' | 'surgery' | 'followup' | 'settings'
14+
15+
interface SampleDataContextType {
16+
sampleContext: TemplateContext
17+
customData: Partial<TemplateContext>
18+
isEditorOpen: boolean
19+
activeTab: TabId
20+
usingRealSettings: boolean
21+
hasCustomizations: boolean
22+
templateType: TemplateType
23+
updateField: (path: string, value: unknown) => void
24+
resetToDefaults: () => void
25+
resetCategory: (category: TabId) => void
26+
toggleEditor: () => void
27+
setEditorOpen: (open: boolean) => void
28+
setActiveTab: (tab: TabId) => void
29+
}
30+
31+
const SampleDataContext = createContext<SampleDataContextType | null>(null)
32+
33+
// Deep merge utility for TemplateContext
34+
function deepMergeContext(
35+
target: TemplateContext,
36+
source: Partial<TemplateContext>
37+
): TemplateContext {
38+
const result: TemplateContext = {
39+
patient: { ...target.patient },
40+
surgery: { ...target.surgery },
41+
settings: { ...target.settings },
42+
followup: target.followup ? { ...target.followup } : undefined
43+
}
44+
45+
if (source.patient) {
46+
result.patient = { ...result.patient, ...source.patient }
47+
}
48+
if (source.surgery) {
49+
result.surgery = { ...result.surgery, ...source.surgery }
50+
}
51+
if (source.settings) {
52+
result.settings = { ...result.settings, ...source.settings }
53+
}
54+
if (source.followup) {
55+
result.followup = result.followup
56+
? { ...result.followup, ...source.followup }
57+
: source.followup
58+
}
59+
60+
return result
61+
}
62+
63+
// Set nested value by path for TemplateContext
64+
function setNestedValue(
65+
obj: Partial<TemplateContext>,
66+
path: string,
67+
value: unknown
68+
): Partial<TemplateContext> {
69+
const keys = path.split('.')
70+
const result = { ...obj } as Record<string, unknown>
71+
72+
let current: Record<string, unknown> = result
73+
for (let i = 0; i < keys.length - 1; i++) {
74+
const key = keys[i]
75+
current[key] = { ...((current[key] as Record<string, unknown>) || {}) }
76+
current = current[key] as Record<string, unknown>
77+
}
78+
79+
current[keys[keys.length - 1]] = value
80+
return result as Partial<TemplateContext>
81+
}
82+
83+
interface SampleDataProviderProps {
84+
templateType: TemplateType
85+
children: ReactNode
86+
}
87+
88+
export const SampleDataProvider = ({ templateType, children }: SampleDataProviderProps) => {
89+
const { settings } = useSettings()
90+
91+
const [customData, setCustomData] = useState<Partial<TemplateContext>>({})
92+
const [isEditorOpen, setIsEditorOpen] = useState(false)
93+
const [activeTab, setActiveTab] = useState<TabId>('patient')
94+
95+
// Check if real settings are available
96+
const realSettings = useMemo(() => {
97+
const hospital = settings.hospital
98+
const unit = settings.unit
99+
const telephone = settings.telephone
100+
101+
return {
102+
hospital: hospital || null,
103+
unit: unit || null,
104+
telephone: telephone || null,
105+
hasAny: Boolean(hospital || unit || telephone)
106+
}
107+
}, [settings])
108+
109+
// Get base sample context with real settings merged
110+
const baseSampleContext = useMemo(() => {
111+
const base = getSampleContext(templateType)
112+
113+
// Merge real settings if available
114+
if (realSettings.hasAny) {
115+
return {
116+
...base,
117+
settings: {
118+
hospital: realSettings.hospital || base.settings.hospital,
119+
unit: realSettings.unit || base.settings.unit,
120+
telephone: realSettings.telephone || base.settings.telephone
121+
}
122+
}
123+
}
124+
125+
return base
126+
}, [templateType, realSettings])
127+
128+
// Merge custom data with base context
129+
const sampleContext = useMemo(() => {
130+
if (Object.keys(customData).length === 0) {
131+
return baseSampleContext
132+
}
133+
return deepMergeContext(baseSampleContext, customData)
134+
}, [baseSampleContext, customData])
135+
136+
// Check if user has made customizations
137+
const hasCustomizations = useMemo(() => {
138+
return Object.keys(customData).length > 0
139+
}, [customData])
140+
141+
const updateField = useCallback((path: string, value: unknown) => {
142+
setCustomData((prev) => setNestedValue(prev, path, value))
143+
}, [])
144+
145+
const resetToDefaults = useCallback(() => {
146+
setCustomData({})
147+
}, [])
148+
149+
const resetCategory = useCallback((category: TabId) => {
150+
setCustomData((prev) => {
151+
const next = { ...prev }
152+
delete next[category as keyof TemplateContext]
153+
return next
154+
})
155+
}, [])
156+
157+
const toggleEditor = useCallback(() => {
158+
setIsEditorOpen((prev) => !prev)
159+
}, [])
160+
161+
const contextValue = useMemo(
162+
() => ({
163+
sampleContext,
164+
customData,
165+
isEditorOpen,
166+
activeTab,
167+
usingRealSettings: realSettings.hasAny,
168+
hasCustomizations,
169+
templateType,
170+
updateField,
171+
resetToDefaults,
172+
resetCategory,
173+
toggleEditor,
174+
setEditorOpen: setIsEditorOpen,
175+
setActiveTab
176+
}),
177+
[
178+
sampleContext,
179+
customData,
180+
isEditorOpen,
181+
activeTab,
182+
realSettings.hasAny,
183+
hasCustomizations,
184+
templateType,
185+
updateField,
186+
resetToDefaults,
187+
resetCategory,
188+
toggleEditor
189+
]
190+
)
191+
192+
return (
193+
<SampleDataContext.Provider value={contextValue}>{children}</SampleDataContext.Provider>
194+
)
195+
}
196+
197+
export const useSampleData = () => {
198+
const context = useContext(SampleDataContext)
199+
200+
if (!context) {
201+
throw new Error('useSampleData must be used within a SampleDataProvider')
202+
}
203+
204+
return context
205+
}

0 commit comments

Comments
 (0)