Skip to content

Commit cab9df5

Browse files
committed
feat: add template selection in print dialog
- Create PrintDialog component with template dropdown - If only one template exists, print directly without dialog - If multiple templates exist, show selection dialog - Pre-selects default template in dropdown - Update view-surgery.tsx to use PrintDialog for both surgery and follow-up printing
1 parent 95a9ca0 commit cab9df5

2 files changed

Lines changed: 248 additions & 49 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { useState, useEffect, useMemo } from 'react'
2+
import { useQuery } from '@tanstack/react-query'
3+
import { Printer, FileText, ChevronDown, Check } from 'lucide-react'
4+
import { queries } from '@renderer/lib/queries'
5+
import { TemplateType, PrintTemplate, TemplateContext } from '../../../../shared/types/template-blocks'
6+
import { Button } from '@renderer/components/ui/button'
7+
import {
8+
Dialog,
9+
DialogContent,
10+
DialogDescription,
11+
DialogFooter,
12+
DialogHeader,
13+
DialogTitle,
14+
DialogTrigger
15+
} from '@renderer/components/ui/dialog'
16+
import {
17+
DropdownMenu,
18+
DropdownMenuContent,
19+
DropdownMenuItem,
20+
DropdownMenuTrigger
21+
} from '@renderer/components/ui/dropdown-menu'
22+
23+
interface PrintDialogProps {
24+
trigger?: React.ReactNode
25+
templateType: TemplateType
26+
title: string
27+
context: TemplateContext
28+
onPrint?: () => void
29+
}
30+
31+
export const PrintDialog = ({
32+
trigger,
33+
templateType,
34+
title,
35+
context,
36+
onPrint
37+
}: PrintDialogProps) => {
38+
const [open, setOpen] = useState(false)
39+
const [selectedTemplate, setSelectedTemplate] = useState<PrintTemplate | null>(null)
40+
const [isPrinting, setIsPrinting] = useState(false)
41+
42+
// Fetch all templates of this type
43+
const { data: templatesData } = useQuery({
44+
...queries.printTemplates.list({ type: templateType }),
45+
enabled: open
46+
})
47+
48+
const templates = useMemo(() => templatesData?.data || [], [templatesData?.data])
49+
const hasMultipleTemplates = templates.length > 1
50+
51+
// Set default template when templates load
52+
useEffect(() => {
53+
if (templates.length > 0 && !selectedTemplate) {
54+
const defaultTemplate = templates.find((t) => t.isDefault) || templates[0]
55+
setSelectedTemplate(defaultTemplate)
56+
}
57+
}, [templates, selectedTemplate])
58+
59+
const handlePrint = async () => {
60+
if (!selectedTemplate) return
61+
62+
setIsPrinting(true)
63+
try {
64+
await window.electronApi.openPrintDialog({
65+
title,
66+
templateStructure: selectedTemplate.structure,
67+
templateContext: context
68+
})
69+
onPrint?.()
70+
setOpen(false)
71+
} finally {
72+
setIsPrinting(false)
73+
}
74+
}
75+
76+
// If there's only one template (or none), print directly without dialog
77+
const handleTriggerClick = async () => {
78+
// Query templates first if not loaded
79+
if (!templatesData) {
80+
setOpen(true)
81+
return
82+
}
83+
84+
if (templates.length <= 1) {
85+
// Direct print without dialog
86+
const template = templates[0]
87+
if (template) {
88+
setIsPrinting(true)
89+
try {
90+
await window.electronApi.openPrintDialog({
91+
title,
92+
templateStructure: template.structure,
93+
templateContext: context
94+
})
95+
onPrint?.()
96+
} finally {
97+
setIsPrinting(false)
98+
}
99+
}
100+
} else {
101+
setOpen(true)
102+
}
103+
}
104+
105+
const defaultTrigger = (
106+
<Button variant="outline" disabled={isPrinting}>
107+
<Printer className="h-4 w-4 mr-2" />
108+
{isPrinting ? 'Printing...' : 'Print'}
109+
</Button>
110+
)
111+
112+
// If we haven't loaded templates yet, show dialog to load them
113+
// Otherwise, only show dialog if multiple templates exist
114+
return (
115+
<Dialog open={open} onOpenChange={setOpen}>
116+
<DialogTrigger asChild onClick={(e) => {
117+
e.preventDefault()
118+
handleTriggerClick()
119+
}}>
120+
{trigger || defaultTrigger}
121+
</DialogTrigger>
122+
<DialogContent className="sm:max-w-md">
123+
<DialogHeader>
124+
<DialogTitle className="flex items-center gap-2">
125+
<Printer className="h-5 w-5" />
126+
Print {templateType === 'surgery' ? 'Surgery Note' : 'Follow-up Note'}
127+
</DialogTitle>
128+
<DialogDescription>
129+
{hasMultipleTemplates
130+
? 'Select a template for printing this record.'
131+
: 'Click Print to generate the document.'}
132+
</DialogDescription>
133+
</DialogHeader>
134+
135+
<div className="py-4">
136+
{hasMultipleTemplates && (
137+
<div className="space-y-2">
138+
<label className="text-sm font-medium text-muted-foreground">
139+
Template
140+
</label>
141+
<DropdownMenu>
142+
<DropdownMenuTrigger asChild>
143+
<Button
144+
variant="outline"
145+
className="w-full justify-between h-10"
146+
>
147+
<div className="flex items-center gap-2">
148+
<FileText className="h-4 w-4 text-muted-foreground" />
149+
<span className="truncate">
150+
{selectedTemplate?.name || 'Select template'}
151+
</span>
152+
{selectedTemplate?.isDefault && (
153+
<span className="text-xs text-muted-foreground">(Default)</span>
154+
)}
155+
</div>
156+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
157+
</Button>
158+
</DropdownMenuTrigger>
159+
<DropdownMenuContent align="start" className="w-[--radix-dropdown-menu-trigger-width]">
160+
{templates.map((template) => (
161+
<DropdownMenuItem
162+
key={template.id}
163+
onClick={() => setSelectedTemplate(template)}
164+
className="flex items-center justify-between gap-2"
165+
>
166+
<div className="flex flex-col gap-0.5 min-w-0">
167+
<span className="truncate">{template.name}</span>
168+
{template.description && (
169+
<span className="text-xs text-muted-foreground truncate">
170+
{template.description}
171+
</span>
172+
)}
173+
</div>
174+
<div className="flex items-center gap-1.5 flex-shrink-0">
175+
{template.isDefault && (
176+
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
177+
Default
178+
</span>
179+
)}
180+
{selectedTemplate?.id === template.id && (
181+
<Check className="h-4 w-4 text-primary" />
182+
)}
183+
</div>
184+
</DropdownMenuItem>
185+
))}
186+
</DropdownMenuContent>
187+
</DropdownMenu>
188+
</div>
189+
)}
190+
191+
{!hasMultipleTemplates && templates.length === 1 && (
192+
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
193+
<FileText className="h-5 w-5 text-muted-foreground" />
194+
<div>
195+
<p className="text-sm font-medium">{templates[0].name}</p>
196+
{templates[0].description && (
197+
<p className="text-xs text-muted-foreground">{templates[0].description}</p>
198+
)}
199+
</div>
200+
</div>
201+
)}
202+
203+
{templates.length === 0 && (
204+
<div className="text-center py-6 text-muted-foreground">
205+
<FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
206+
<p className="text-sm">No templates available for this type.</p>
207+
<p className="text-xs">Create a template in Settings → Print Templates.</p>
208+
</div>
209+
)}
210+
</div>
211+
212+
<DialogFooter>
213+
<Button variant="outline" onClick={() => setOpen(false)}>
214+
Cancel
215+
</Button>
216+
<Button
217+
onClick={handlePrint}
218+
disabled={!selectedTemplate || isPrinting}
219+
>
220+
<Printer className="h-4 w-4 mr-2" />
221+
{isPrinting ? 'Printing...' : 'Print'}
222+
</Button>
223+
</DialogFooter>
224+
</DialogContent>
225+
</Dialog>
226+
)
227+
}

src/renderer/src/routes/surgeries/view-surgery.tsx

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ import {
4242
import { DoctorModel } from '@shared/models/DoctorModel'
4343
import { useSettings } from '@renderer/contexts/SettingsContext'
4444
import { createSurgeryContext, createFollowupContext } from '@renderer/lib/print'
45-
import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent'
46-
import { PrintTemplate } from 'src/shared/types/template-blocks'
45+
import { PrintDialog } from '@renderer/components/print/PrintDialog'
4746

4847
const getSurgeryByIdQuery = (id: number) => queries.surgeries.get(id)
4948
const getSurgeryFollowupsQuery = (surgeryId: number) => queries.surgeries.getFollowups(surgeryId)
@@ -143,10 +142,9 @@ interface FollowupCardProps {
143142
patient: PatientModel
144143
surgery: SurgeryModel
145144
settings?: Record<string, string | null>
146-
followupTemplate?: PrintTemplate | null
147145
}
148146

149-
const FollowupCard = ({ followup, patient, surgery, settings, followupTemplate }: FollowupCardProps) => {
147+
const FollowupCard = ({ followup, patient, surgery, settings }: FollowupCardProps) => {
150148
const queryClient = useQueryClient()
151149
const deleteFollowupMutation = useMutation({
152150
mutationFn: () => unwrapResult(window.api.invoke('deleteFollowUp', followup.id)),
@@ -157,18 +155,7 @@ const FollowupCard = ({ followup, patient, surgery, settings, followupTemplate }
157155
}
158156
})
159157

160-
const handlePrintFollowup = async () => {
161-
if (!followupTemplate) {
162-
console.warn('No followup template available')
163-
return
164-
}
165-
const context = createFollowupContext(patient, surgery, followup, settings)
166-
await window.electronApi.openPrintDialog({
167-
title: `${patient.name || 'Patient'} - Follow-up`,
168-
templateStructure: followupTemplate.structure,
169-
templateContext: context
170-
})
171-
}
158+
const followupContext = createFollowupContext(patient, surgery, followup, settings)
172159

173160
return (
174161
<div className="group relative p-4 rounded-xl border bg-card hover:bg-accent/30 transition-colors">
@@ -179,9 +166,16 @@ const FollowupCard = ({ followup, patient, surgery, settings, followupTemplate }
179166
<span>{formatDateTime(followup.created_at)}</span>
180167
</div>
181168
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
182-
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handlePrintFollowup}>
183-
<Printer className="h-3.5 w-3.5" />
184-
</Button>
169+
<PrintDialog
170+
templateType="followup"
171+
title={`${patient.name || 'Patient'} - Follow-up`}
172+
context={followupContext}
173+
trigger={
174+
<Button variant="ghost" size="icon" className="h-7 w-7">
175+
<Printer className="h-3.5 w-3.5" />
176+
</Button>
177+
}
178+
/>
185179
<AddOrEditFollowup
186180
trigger={
187181
<Button variant="ghost" size="icon" className="h-7 w-7">
@@ -263,15 +257,6 @@ export const ViewSurgery = () => {
263257
enabled: !!surgeryId
264258
})
265259

266-
// Fetch default print templates
267-
const { data: surgeryTemplate } = useQuery({
268-
...queries.printTemplates.getDefault('surgery')
269-
})
270-
271-
const { data: followupTemplate } = useQuery({
272-
...queries.printTemplates.getDefault('followup')
273-
})
274-
275260
const ptName = useMemo(
276261
() => patient?.name || patient?.phn || 'Patient',
277262
[patient?.name, patient?.phn]
@@ -286,22 +271,7 @@ export const ViewSurgery = () => {
286271
setBreadcrumbs([{ label: 'Surgeries', to: '/surgeries' }])
287272
}, [setBreadcrumbs])
288273

289-
const handlePrint = async () => {
290-
if (!patient || !surgery || !surgeryTemplate) return
291-
292-
const context = createSurgeryContext(patient, surgery, settings)
293-
await window.electronApi.openPrintDialog({
294-
title: `${ptName} - ${surgeryName}`,
295-
templateStructure: surgeryTemplate.structure,
296-
templateContext: context
297-
})
298-
}
299-
300-
useKeyboardEvent({
301-
key: 'p',
302-
ctrlKey: true,
303-
onKeyDown: handlePrint
304-
})
274+
const surgeryContext = patient && surgery ? createSurgeryContext(patient, surgery, settings) : null
305275

306276
const noFollowups = !isFollowupLoading && followups && followups.length === 0
307277

@@ -327,10 +297,13 @@ export const ViewSurgery = () => {
327297
</div>
328298
</div>
329299
<div className="flex items-center gap-2">
330-
<Button variant="outline" onClick={handlePrint}>
331-
<Printer className="h-4 w-4 mr-2" />
332-
Print
333-
</Button>
300+
{surgeryContext && (
301+
<PrintDialog
302+
templateType="surgery"
303+
title={`${ptName} - ${surgeryName}`}
304+
context={surgeryContext}
305+
/>
306+
)}
334307
<Button
335308
variant="gradient"
336309
onClick={() => navigate(`/patients/${patientId}/surgeries/${surgeryId}/edit`)}
@@ -524,7 +497,6 @@ export const ViewSurgery = () => {
524497
patient={patient}
525498
surgery={surgery}
526499
settings={settings}
527-
followupTemplate={followupTemplate}
528500
/>
529501
))}
530502
</div>

0 commit comments

Comments
 (0)