Skip to content

Commit

Permalink
feat: print
Browse files Browse the repository at this point in the history
  • Loading branch information
kasvith committed Jun 13, 2024
1 parent 7227028 commit 70832a0
Show file tree
Hide file tree
Showing 18 changed files with 715 additions and 32 deletions.
10 changes: 9 additions & 1 deletion electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export default defineConfig({
'@': resolve(__dirname, 'src/renderer/src')
}
},
plugins: [react()]
plugins: [react()],
build: {
rollupOptions: {
input: {
browser: resolve(__dirname, 'src/renderer/index.html'),
print: resolve(__dirname, 'src/renderer/print.html')
}
}
}
}
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"autoprefixer": "^10.4.19",
"electron": "^28.2.0",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"electron-vite": "^2.2.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.2",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions resources/templates/surgery-opnote.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<h1 class='text-2xl text-center bold'>
{{settings.hospital}}
</h1>
<h2 class='text-lg pt-1 text-center'>
{{settings.unit}}
</h2>

<div class='grid grid-cols-3 gap-2 mt-3 p-3'>
<p>
<span class='bold'>
Name:
</span>
{{patient.name}}
</p>
<p>
<span class='bold'>
Age:
</span>
{{patient.age}}
</p>
<p>
<span class='bold'>
BHT:
</span>
{{surgery.bht}}
</p>
<p>
<span class='bold'>
Ward:
</span>
{{surgery.ward}}
</p>
<p>
<span class='bold'>
Date:
</span>
{{surgery.date}}
</p>
</div>

<hr />

<h2 class='text-xl mt-3 text-center p-3'>
{{surgery.title}}
</h2>

<div class='flex justify-end items-end flex-col'>
<div>
{{#if surgery.doneBy.length}}
<div class='p-1'>
<div class='bold m-1'>
Done By:
</div>
<ul class='disc'>
{{#each surgery.doneBy as |doctor|}}
<li>
{{doctor}}
</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if surgery.assistedBy.length}}
<div class='p-1'>
<div class='bold m-1'>
Assisted By:
</div>
<ul class='disc'>
{{#each surgery.assistedBy as |doctor|}}
<li>
{{doctor}}
</li>
{{/each}}
</ul>
</div>
{{/if}}
</div>
</div>

{{#if surgery.notes}}
<div class='p-1'>
<div class='prose'>
{{{surgery.notes}}}
</div>
</div>
{{/if}}

{{#if surgery.post_op_notes}}
<hr />
<div class='p-1'>
<h2 class='text-lg mt-3 text-center p-3'>
Post-Op Notes
</h2>
<div class='prose'>
{{{surgery.post_op_notes}}}
</div>
</div>
{{/if}}
21 changes: 18 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { db, migrateToLatest } from './db'
import { registerApi } from './api'

import electronUpdater, { type AppUpdater } from 'electron-updater'
import { PrintDialogArgs } from '../preload/interfaces'

export function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
Expand Down Expand Up @@ -105,11 +106,9 @@ function createPrinterWindow() {
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
workerWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/print.html`)
} else {
workerWindow.loadFile(join(__dirname, '../renderer/print.html'), { hash: '/print' })
workerWindow.loadFile(join(__dirname, '../renderer/print.html'))
}

workerWindow.hide()

return workerWindow
}

Expand Down Expand Up @@ -147,6 +146,22 @@ app.whenReady().then(() => {

return true
})

ipcMain.handle('printDialog', async (event, options: PrintDialogArgs) => {
const workerWindow = createPrinterWindow()
workerWindow.once('ready-to-show', () => {
const windowTitle = options.title || 'Print Preview'
workerWindow.setTitle(windowTitle)

workerWindow.webContents.send('printData', options)
workerWindow.show()
// open devtools

if (is.dev) {
workerWindow.webContents.openDevTools()
}
})
})
})

// Quit when all windows are closed, except on macOS. There, it's common
Expand Down
3 changes: 3 additions & 0 deletions src/preload/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import type { ApiType } from '../main/api'
import { PrintDialogArgs } from './interfaces'

type ElectronApi = ElectronAPI & {
getAppVersion: () => Promise<string>
boot: () => Promise<boolean>
openPrintDialog: (options: PrintDialogArgs) => Promise<void>
onPrintData: (callback: (options: PrintDialogArgs) => void) => void
}

declare global {
Expand Down
12 changes: 11 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { PrintDialogArgs } from './interfaces'

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
Expand All @@ -21,10 +22,19 @@ const boot = async () => {
return await ipcRenderer.invoke('boot')
}

const openPrintDialog = async (options: PrintDialogArgs) => {
return await ipcRenderer.invoke('printDialog', options)
}

const onPrintData = (callback: (options: PrintDialogArgs) => void) =>
ipcRenderer.on('printData', (_, options) => callback(options))

const electronApi = {
...electronAPI,
getAppVersion,
boot
boot,
openPrintDialog,
onPrintData
}

if (process.contextIsolated) {
Expand Down
4 changes: 4 additions & 0 deletions src/preload/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface PrintDialogArgs {
title?: string
data?: object
}
17 changes: 10 additions & 7 deletions src/renderer/print.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
<!doctype html>
<html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<title>OpNotes</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Print Preview</title>

<link rel="stylesheet" href="/src/styles/reset.css" />
<link rel="stylesheet" href="/src/styles/screen.css" />
<link rel="stylesheet" href="/src/styles/print.css" media="print" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" />
</head>

<body class="overflow-hidden p-0 m-0">
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/print.tsx"></script>
</body>

</html>
30 changes: 30 additions & 0 deletions src/renderer/src/lib/print.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PatientModel } from '@shared/models/PatientModel'
import { SurgeryModel } from '@shared/models/SurgeryModel'
import { formatDate, isEmptyHtml } from './utils'

export const surgeryPrintData = (
patient?: PatientModel,
surgery?: SurgeryModel,
settings?: Record<string, string | null>
) => ({
patient: {
...patient,
age: patient?.age ? `${patient?.age} yrs` : `<1 yrs`
},
surgery: {
...surgery,
doneBy: surgery?.doneBy
? surgery?.doneBy.map((doctor) => `Dr. ${doctor.name} (${doctor.designation})`)
: [],
assistedBy: surgery?.assistedBy
? surgery?.assistedBy.map((doctor) => `Dr. ${doctor.name} (${doctor.designation})`)
: [],
date: surgery?.date ? formatDate(surgery?.date) : null,
notes: isEmptyHtml(surgery?.notes) ? null : surgery?.notes,
post_op_notes: isEmptyHtml(surgery?.post_op_notes) ? null : surgery?.post_op_notes
},
settings: {
hospital: settings?.hospital || '',
unit: settings?.unit || ''
}
})
12 changes: 12 additions & 0 deletions src/renderer/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ export const formatDate = (date?: Date) => dayjs(date).format('DD/MM/YYYY')
export const formatDateTime = (date?: Date) => dayjs(date).format('DD/MM/YYYY HH:mm')

export const formatTime = (date?: Date) => dayjs(date).format('HH:mm')

export const isEmptyString = (str?: string | null) => str && str.trim().length === 0

export const isEmptyHtml = (html?: string | null) => {
if (!html) {
return true
}

const tempEl = document.createElement('div')
tempEl.innerHTML = html
return !tempEl.textContent?.trim()
}
4 changes: 0 additions & 4 deletions src/renderer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,6 @@ const router = createHashRouter([
element: <ErrorPage />
}
]
},
{
path: '/print',
element: <div className="">Printing</div>
}
])

Expand Down
60 changes: 60 additions & 0 deletions src/renderer/src/print.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import React, { useEffect, useMemo } from 'react'
import ReactDOM from 'react-dom/client'
import handlebars from 'handlebars'
import surgeryOpNoteTemplate from '../../../resources/templates/surgery-opnote.hbs?raw'
import { Button } from './components/ui/button'

const printTemplate = handlebars.compile(surgeryOpNoteTemplate)

const Print = () => {
const [printData, setPrintData] = React.useState<object | null>(null)
useEffect(() => {
const handlePrintData = (data) => {
setPrintData(data)
}

window.electronApi.onPrintData(handlePrintData)
}, [])

const printHtml = useMemo(() => {
if (!printData) {
return ''
}
return printTemplate((printData as any).data)
}, [printData])

const handlePrint = () => {
window.print()
}

useEffect(() => {
const keyboardListener = (e: KeyboardEvent) => {
if (e.key === 'p' && e.ctrlKey) {
handlePrint()
}
}

window.addEventListener('keydown', keyboardListener)

return () => {
window.removeEventListener('keydown', keyboardListener)
}
}, [])

return (
<div className="w-full">
<div className="flex justify-end print-hidden">
<Button onClick={handlePrint}>Print</Button>
</div>
{printData && <div dangerouslySetInnerHTML={{ __html: printHtml }}></div>}
</div>
)
}

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Print />
</React.StrictMode>
)
Loading

0 comments on commit 70832a0

Please sign in to comment.