Skip to content

Commit e9aa47e

Browse files
authored
feat: add doctor view page with surgery history (#15)
* feat: add doctor view page with surgery history - Add /doctors/:id view page showing doctor info and surgeries - Add listSurgeriesByDoctorId API to query surgeries by doctor role - Support filtering by role (Surgeon/Assistant/All) with tabs - Update doctor list and command palette to navigate to view page - Fix query invalidation after editing doctor * chore: update WhatsApp community invite link * feat: add WhatsApp QR code to community section - Generate QR code for WhatsApp invite link - Display QR code in Support > Community section - Add WhatsApp link and QR to README - Remove WhatsApp from issue template config * refactor: centralize external links in shared constants
1 parent 63a3eb4 commit e9aa47e

16 files changed

Lines changed: 804 additions & 28 deletions

File tree

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
blank_issues_enabled: false
22
contact_links:
3-
- name: WhatsApp Community
4-
url: https://chat.whatsapp.com/YOUR_INVITE_LINK
5-
about: Join our WhatsApp community for quick support and discussions
63
- name: GitHub Discussions
74
url: https://github.com/wavezync/opnotes/discussions
85
about: Ask questions, share ideas, or discuss features with the community

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ src/
7575
└── shared/ # Shared types and models
7676
```
7777

78+
## Community
79+
80+
Join our WhatsApp community to get instant support, share feedback, and connect with other users.
81+
82+
<a href="https://chat.whatsapp.com/L4LMwxsbjPk514I1ToRris">
83+
<img src="resources/whatsapp-qr.png" alt="WhatsApp Community QR Code" width="150" />
84+
</a>
85+
86+
[Join WhatsApp Community](https://chat.whatsapp.com/L4LMwxsbjPk514I1ToRris)
87+
7888
## License
7989

8090
MIT

agents/nightly-ci-plan.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Nightly Distribution Channel Implementation Plan
2+
3+
**Date:** 2026-01-19
4+
**Status:** Approved for implementation
5+
6+
## Overview
7+
8+
Add a manually-triggered GitHub Actions workflow for nightly/experimental builds, separate from the existing tagged release workflow.
9+
10+
## Key Design Decisions
11+
12+
1. **Manual trigger only** - Uses `workflow_dispatch` with optional inputs
13+
2. **Rolling `nightly-latest` tag** - Single pre-release that gets replaced on each build
14+
3. **Version stamping** - Temporarily modifies `package.json` for build only (e.g., `0.0.11-nightly.20260119`)
15+
4. **Pre-release flag** - Ensures auto-updater channel separation without code changes
16+
5. **No changes to existing workflow** - Stable releases continue working as-is
17+
18+
## Files to Create
19+
20+
### `.github/workflows/nightly.yaml`
21+
22+
New workflow with:
23+
- `workflow_dispatch` trigger with inputs:
24+
- `version_suffix` - Optional custom suffix (defaults to date-based)
25+
- `release_notes` - Notes for the release
26+
- `replace_existing` - Whether to delete existing nightly release (default: true)
27+
- Prepare job: generates version, deletes existing nightly if needed
28+
- Build jobs: matrix build for ubuntu/macos/windows (same as release.yaml)
29+
- Release job: downloads artifacts, creates single pre-release
30+
31+
Key differences from `release.yaml`:
32+
- Uses artifact upload/download pattern to consolidate all platform builds into one release
33+
- Sets `prerelease: true` instead of `draft: true`
34+
- Stamps version before build: `npm pkg set version="${NIGHTLY_VERSION}"`
35+
- Fixed tag `nightly-latest` that gets replaced
36+
37+
## Workflow Structure
38+
39+
```yaml
40+
name: Nightly Build
41+
42+
on:
43+
workflow_dispatch:
44+
inputs:
45+
version_suffix:
46+
description: 'Version suffix (empty = auto date-based)'
47+
required: false
48+
default: ''
49+
release_notes:
50+
description: 'Release notes'
51+
required: false
52+
default: 'Experimental nightly build for testing purposes.'
53+
replace_existing:
54+
description: 'Replace existing nightly-latest release'
55+
required: false
56+
type: boolean
57+
default: true
58+
59+
jobs:
60+
prepare: # Generate version, delete existing release
61+
build: # Matrix build (ubuntu, macos, windows) with artifact upload
62+
release: # Download artifacts, create pre-release
63+
```
64+
65+
## Version Format
66+
67+
- Default: `{base_version}-nightly.{YYYYMMDD}` → `0.0.11-nightly.20260119`
68+
- Custom: `{base_version}-{suffix}` → `0.0.11-alpha.1` or `0.0.11-rc.1`
69+
70+
## Release Details
71+
72+
| Property | Value |
73+
|----------|-------|
74+
| Tag | `nightly-latest` |
75+
| Name | `Nightly Build - 0.0.11-nightly.20260119` |
76+
| Pre-release | Yes |
77+
| Draft | No |
78+
79+
## How to Trigger
80+
81+
**GitHub UI:**
82+
1. Go to Actions → Nightly Build → Run workflow
83+
2. Optionally customize inputs
84+
3. Click "Run workflow"
85+
86+
**CLI:**
87+
```bash
88+
gh workflow run nightly.yaml
89+
```
90+
91+
## No Changes Required To
92+
93+
- `electron-builder.yml` - Version in artifact names handled automatically
94+
- `package.json` - Base version remains unchanged in repo
95+
- `src/main/updater.ts` - Pre-release flag handles channel separation
96+
- Existing `release.yaml` - Stable workflow unchanged
97+
98+
## Future Enhancements (Optional)
99+
100+
If users need to opt into nightly auto-updates:
101+
1. Add `update_channel` setting in app_settings
102+
2. Modify `updater.ts` to set `allowPrerelease` based on setting
103+
3. Add UI toggle in Settings
104+
105+
## Verification
106+
107+
1. Trigger workflow manually from GitHub Actions
108+
2. Verify all three platform builds complete
109+
3. Check GitHub Releases for new `nightly-latest` pre-release
110+
4. Download and test installer from each platform
111+
5. Verify auto-updater on stable installs does NOT offer nightly update

resources/whatsapp-qr.png

1.59 KB
Loading

src/main/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getFollowUpsBySurgeryId,
2525
getSurgeryById,
2626
listSurgeries,
27+
listSurgeriesByDoctorId,
2728
lookupSurgery,
2829
updateFollowUp,
2930
updateSurgery,
@@ -67,6 +68,7 @@ export const api = {
6768
getSurgeryById,
6869
lookupSurgery,
6970
listSurgeries,
71+
listSurgeriesByDoctorId,
7072
updateSurgery,
7173
updateSurgeryDoctorsAssistedBy,
7274
updateSurgeryDoctorsDoneBy,

src/main/repository/surgery.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,172 @@ export const deleteSurgeryById = async (id: number) => {
395395

396396
return trx
397397
}
398+
399+
export interface DoctorSurgeryResult {
400+
id: number
401+
patient_id: number
402+
bht: string
403+
title: string | null
404+
ward: string | null
405+
date: string | null
406+
created_at: string
407+
updated_at: string
408+
patient_name: string | null
409+
role: 'done_by' | 'assisted_by'
410+
}
411+
412+
export const listSurgeriesByDoctorId = async (
413+
doctorId: number,
414+
filter: SurgeryFilter & { role?: 'done_by' | 'assisted_by' | 'all' }
415+
) => {
416+
const {
417+
search,
418+
ward,
419+
start_date,
420+
end_date,
421+
pageSize = 50,
422+
page = 0,
423+
sortBy = 'date',
424+
sortOrder = 'desc',
425+
role = 'all'
426+
} = filter
427+
428+
// Get surgeries where doctor is in done_by table
429+
const doneByQuery = db
430+
.selectFrom('surgeries')
431+
.innerJoin('surgery_doctors_done_by', 'surgery_doctors_done_by.surgery_id', 'surgeries.id')
432+
.leftJoin('patients', 'patients.id', 'surgeries.patient_id')
433+
.where('surgery_doctors_done_by.doctor_id', '=', doctorId)
434+
.select([
435+
'surgeries.id',
436+
'surgeries.patient_id',
437+
'surgeries.bht',
438+
'surgeries.title',
439+
'surgeries.ward',
440+
'surgeries.date',
441+
'surgeries.created_at',
442+
'surgeries.updated_at',
443+
'patients.name as patient_name',
444+
sql<'done_by' | 'assisted_by'>`'done_by'`.as('role')
445+
])
446+
447+
// Get surgeries where doctor is in assisted_by table
448+
const assistedByQuery = db
449+
.selectFrom('surgeries')
450+
.innerJoin(
451+
'surgery_doctors_assisted_by',
452+
'surgery_doctors_assisted_by.surgery_id',
453+
'surgeries.id'
454+
)
455+
.leftJoin('patients', 'patients.id', 'surgeries.patient_id')
456+
.where('surgery_doctors_assisted_by.doctor_id', '=', doctorId)
457+
.select([
458+
'surgeries.id',
459+
'surgeries.patient_id',
460+
'surgeries.bht',
461+
'surgeries.title',
462+
'surgeries.ward',
463+
'surgeries.date',
464+
'surgeries.created_at',
465+
'surgeries.updated_at',
466+
'patients.name as patient_name',
467+
sql<'done_by' | 'assisted_by'>`'assisted_by'`.as('role')
468+
])
469+
470+
// Build the combined query based on role filter
471+
let combinedQuery
472+
if (role === 'done_by') {
473+
combinedQuery = doneByQuery
474+
} else if (role === 'assisted_by') {
475+
combinedQuery = assistedByQuery
476+
} else {
477+
// Union both queries for 'all' role
478+
combinedQuery = doneByQuery.union(assistedByQuery)
479+
}
480+
481+
// Wrap in a subquery for filtering and sorting
482+
let query = db.selectFrom(combinedQuery.as('doctor_surgeries')).selectAll()
483+
484+
// Apply search filter using bht or title
485+
if (search) {
486+
const searchTerm = `%${search}%`
487+
query = query.where((eb) =>
488+
eb.or([
489+
eb('bht', 'like', searchTerm),
490+
eb('title', 'like', searchTerm),
491+
eb('patient_name', 'like', searchTerm)
492+
])
493+
)
494+
}
495+
496+
if (ward) {
497+
query = query.where('ward', '=', ward)
498+
}
499+
500+
if (start_date) {
501+
query = query.where('date', '>=', start_date as unknown as string)
502+
}
503+
504+
if (end_date) {
505+
query = query.where('date', '<=', end_date as unknown as string)
506+
}
507+
508+
// Apply sorting
509+
switch (sortBy) {
510+
case 'title':
511+
query = query.orderBy('title', sortOrder)
512+
break
513+
case 'bht':
514+
query = query.orderBy('bht', sortOrder)
515+
break
516+
case 'ward':
517+
query = query.orderBy('ward', sortOrder)
518+
break
519+
case 'created_at':
520+
query = query.orderBy('created_at', sortOrder)
521+
break
522+
case 'updated_at':
523+
query = query.orderBy('updated_at', sortOrder)
524+
break
525+
case 'date':
526+
default:
527+
query = query.orderBy('date', sortOrder)
528+
break
529+
}
530+
531+
const surgeries = await query.limit(pageSize).offset(page * pageSize).execute()
532+
533+
// Get total count
534+
const countQuery = db.selectFrom(combinedQuery.as('doctor_surgeries')).select((eb) =>
535+
eb.fn.countAll<number>().as('total')
536+
)
537+
538+
let filteredCountQuery = countQuery
539+
540+
if (search) {
541+
const searchTerm = `%${search}%`
542+
filteredCountQuery = db
543+
.selectFrom(combinedQuery.as('doctor_surgeries'))
544+
.where((eb) =>
545+
eb.or([
546+
eb('bht', 'like', searchTerm),
547+
eb('title', 'like', searchTerm),
548+
eb('patient_name', 'like', searchTerm)
549+
])
550+
)
551+
.select((eb) => eb.fn.countAll<number>().as('total'))
552+
}
553+
554+
if (ward) {
555+
filteredCountQuery = db
556+
.selectFrom(combinedQuery.as('doctor_surgeries'))
557+
.where('ward', '=', ward)
558+
.select((eb) => eb.fn.countAll<number>().as('total'))
559+
}
560+
561+
const totalResult = await filteredCountQuery.executeTakeFirst()
562+
const total = totalResult?.total ?? 0
563+
const pages = Math.ceil(total / pageSize)
564+
565+
return { data: surgeries as DoctorSurgeryResult[], total, pages }
566+
}

src/renderer/src/components/command-palette/CommandPalette.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
140140
<CommandItem
141141
key={`doctor-${doctor.id}`}
142142
value={`doctor ${doctor.name} ${doctor.slmc_reg_no}`}
143-
onSelect={() => runCommand(() => navigate(`/doctors/${doctor.id}/edit`))}
143+
onSelect={() => runCommand(() => navigate(`/doctors/${doctor.id}`))}
144144
>
145145
<UserCog className="mr-2 h-4 w-4 text-purple-500" />
146146
<div className="flex flex-col">

src/renderer/src/components/support/CommunitySection.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Card, CardContent } from '@renderer/components/ui/card'
22
import { Button } from '@renderer/components/ui/button'
33
import { MessageCircle, Bell, HelpCircle, Users, ExternalLink } from 'lucide-react'
4+
import { EXTERNAL_LINKS } from '@shared/constants/links'
5+
import whatsappQr from '../../../../../resources/whatsapp-qr.png?asset'
46

57
export const CommunitySection = () => {
68
const benefits = [
@@ -50,23 +52,34 @@ export const CommunitySection = () => {
5052
>
5153
<CardContent className="pt-8 pb-8 relative">
5254
<div className="absolute top-0 right-0 w-48 h-48 bg-green-500/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl" />
53-
<div className="flex flex-col items-center text-center relative">
54-
<div className="h-16 w-16 rounded-2xl bg-green-500/20 flex items-center justify-center mb-4 transition-transform duration-200 hover:scale-105">
55-
<MessageCircle className="h-8 w-8 text-green-500" />
55+
<div className="flex flex-col md:flex-row items-center gap-6 relative">
56+
{/* QR Code */}
57+
<div className="flex-shrink-0">
58+
<div className="p-3 bg-white rounded-xl shadow-sm">
59+
<img src={whatsappQr} alt="WhatsApp QR Code" className="w-32 h-32" />
60+
</div>
61+
<p className="text-xs text-muted-foreground text-center mt-2">Scan to join</p>
62+
</div>
63+
64+
{/* Content */}
65+
<div className="flex flex-col items-center md:items-start text-center md:text-left">
66+
<div className="h-12 w-12 rounded-xl bg-green-500/20 flex items-center justify-center mb-3">
67+
<MessageCircle className="h-6 w-6 text-green-500" />
68+
</div>
69+
<h3 className="text-xl font-bold mb-2">WhatsApp Community</h3>
70+
<p className="text-muted-foreground max-w-sm mb-4">
71+
Join our active WhatsApp community to get instant support, share feedback,
72+
and connect with healthcare professionals using Op Notes.
73+
</p>
74+
<Button
75+
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
76+
onClick={() => window.open(EXTERNAL_LINKS.WHATSAPP_COMMUNITY, '_blank')}
77+
>
78+
<MessageCircle className="h-4 w-4 mr-2" />
79+
Join WhatsApp Community
80+
<ExternalLink className="h-3 w-3 ml-2" />
81+
</Button>
5682
</div>
57-
<h3 className="text-xl font-bold mb-2">WhatsApp Community</h3>
58-
<p className="text-muted-foreground max-w-sm mb-6">
59-
Join our active WhatsApp community to get instant support, share feedback,
60-
and connect with healthcare professionals using Op Notes.
61-
</p>
62-
<Button
63-
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
64-
onClick={() => window.open('https://chat.whatsapp.com/YOUR_INVITE_LINK', '_blank')}
65-
>
66-
<MessageCircle className="h-4 w-4 mr-2" />
67-
Join WhatsApp Community
68-
<ExternalLink className="h-3 w-3 ml-2" />
69-
</Button>
7083
</div>
7184
</CardContent>
7285
</Card>

0 commit comments

Comments
 (0)