09-communicationswave: W5filled13 citations

Notes, Tasks and Activity Feed

Audiences: doctor, developer

Notes, Tasks and Activity Feed

Clinic staff capture unstructured thoughts (rich text, voice recordings, file attachments), coordinate work via task groups with STT-powered notes, and review a personal activity timeline that auto-tracks resource views — all wired to SSE for live UI updates.

Business Purpose

Clinical workflows generate a constant stream of context that doesn't belong in structured FHIR records but must not be lost: a dictated voice memo after a consultation, a follow-up checklist for a nurse, a scanned referral letter flagged for review. The Notes + Tasks + Activity triad covers this gap:

  • Notes: personal, per-user scratchpad with folders, tags, rich-text or plain content, linked audio recordings (with async STT transcription), file attachments (with OCR extraction), and polymorphic links to any clinical entity (patient, visit, appointment, etc.).
  • Tasks: structured work items grouped into TaskGroup containers. Staff assign tasks to colleagues, set priorities and due dates, attach notes with voice recordings, and link tasks to FHIR entities. Categories map to clinical workflows (LAB_REVIEW, APPROVAL_REQUIRED, FOLLOW_UP, etc.).
  • Activity Feed: a rolling, user-scoped timeline of resource views (currently: patient profile accesses), auto-populated by the ActivityInterceptor without any developer instrumentation per endpoint. Used for quick "recently viewed" navigation and audit support.

Audiences

  • Investor: Demonstrates that Dudoxx HMS goes beyond structured EHR data — the notes/tasks layer captures the informal coordination that typically leaks to personal email or WhatsApp, keeping all clinical context on-platform.
  • Clinical buyer (doctor/nurse/receptionist): Dictate a voice note after a visit, get an auto-appended transcript; assign a follow-up task to a nurse with a due date; open "Recent Activity" to jump back to the last three patients you viewed.
  • Developer/partner: Three independent NestJS modules under src/misc/; each has its own controller, service(s), and Prisma models in prisma-main. Notes exposes additional sub-services for search, folders, links, attachments, and transcription. All SSE events use the shared EventBusService on user:{userId} channels.
  • Internal (ops/support): Activity cleanup cron runs daily at 02:00 UTC and purges records older than 90 days. Notes and tasks are tenant-scoped via organizationId on every Prisma query.

Architecture

Notes

NotesController → six injected services, all operating on ddx_api_main:

NotesController owns:

ServiceResponsibility
NotesServiceCRUD on Note model, folder + tag + link includes
NotesSearchServicefull-text search via Prisma (title, contentText, tags)
NoteFolderServicehierarchical NoteFolder tree (recursive parentFolderId)
NoteLinkServicepolymorphic links: Note{Patient|Visit|Appointment|...}
NotesTranscriptionServiceasync STT via TranscriptionService (voice-video module)
NoteAttachmentServiceupload to MinIO, trigger OCR pipeline

NotesTranscriptionService delegates to TranscriptionService from src/voice-video/recordings/ — the same Deepgram/Gladia STT pipeline used for visit recordings. On completion it appends \n\n--- Transcription ---\n<transcript> to Note.contentText and fires NOTE_TRANSCRIPTION_COMPLETED SSE. OCR results from NoteAttachmentService are similarly appended via onOcrComplete with NOTE_OCR_COMPLETED SSE.

Tasks

ControllerServiceResponsibility
TasksControllerTasksServiceTaskGroup CRUD with items, assignees, notes (with STT), entity links

A TaskGroup is the top-level container (title, category, priority, due date). TaskItem records are nested work items. TaskAssignee binds users to a group with a role. TaskNote supports voice recordings (STT optional). TaskLink is a polymorphic join to clinical entities (PATIENT, VISIT, APPOINTMENT, DOCUMENT, etc.) — no FK constraint, service-layer validated, mirroring the NoteLink pattern.

Activity Feed

ComponentWiring
ActivityInterceptorglobal APP_INTERCEPTOR position 6ActivityServiceprisma.recentActivity.create
ActivityControllerActivityServicefindMany / deleteMany

The interceptor fires post-response (via RxJS tap) on GET /patients/:id requests, writing a recentActivity row without adding latency to the response path (setImmediate). Service-account users (non-UUID IDs) are skipped silently. A daily cron at 02:00 UTC purges entries older than 90 days.

Tech Stack & Choices

ComponentChoiceWhy
Persistenceprisma-main (ddx_api_main)All three features share the main schema; models at notes.prisma, tasks.prisma, user-activity.prisma
STT for notes/tasksTranscriptionService (voice-video module)Re-uses the existing Deepgram/Gladia pipeline — no duplicate STT logic
OCR for note attachmentsOcrResult model (prisma-main) linked to AttachmentSame OCR pipeline as document processing
Real-time updatesSSE EventBusService on user:{userId} channelsConsistent with the rest of the DDX event bus; no separate WebSocket needed
Note contentJson (rich-text AST) + String (contentText plain-text copy)JSON for editor rendering; contentText for full-text search without JSON parsing
Activity trackingPost-response interceptor (setImmediate)Zero-latency cost on the critical response path
Task categoriesTaskCategory enum (9 values)Maps to clinical workflow stages; CUSTOM covers ad-hoc use

Data Flow

Notes: voice dictation → transcription → SSE append

  1. Staff uploads audio recording linked to a note: POST /notes/:noteId/recordings/:recordingId/transcribe.
  2. NotesTranscriptionService.startTranscription validates both the Recording and the Note exist (tenant-scoped Prisma queries).
  3. SSE event NOTE_TRANSCRIPTION_STARTED fires immediately on user:{userId} channel — UI shows a spinner.
  4. runTranscription is kicked off asynchronously (.catch logs + fires NOTE_TRANSCRIPTION_FAILED).
  5. On completion: onTranscriptionComplete appends the transcript text to Note.contentText with a --- Transcription --- separator; fires NOTE_TRANSCRIPTION_COMPLETED.
  6. OCR for attachments follows the same pattern via onOcrCompleteNOTE_OCR_COMPLETED.
  1. POST /tasks with CreateTaskGroupDto — includes optional items[], assigneeIds[], links[] arrays.
  2. TasksService.create resolves organizationId slug → UUID via OrganizationResolverService.
  3. TaskGroup created; then items, assignees (creator auto-added as OWNER), and entity links created in follow-up transactions.
  4. All Prisma queries carry organizationId for tenant isolation.

Activity: auto-track on patient page load

  1. Browser navigates to patient profile → Next.js server action → GET /patients/:id.
  2. ActivityInterceptor.intercept fires post-response: setImmediate(() => activityService.trackActivity(...)).
  3. ActivityService.trackActivity skips non-UUID user IDs (service accounts); writes recentActivity row with activityType: 'viewed_patient', resourceType: 'Patient', resourceId.
  4. GET /activity/recent returns the last 50 entries for the current user.

Implicated Code

  • ddx-api/src/misc/notes/notes.service.ts:39NotesService.findAll — tenant-scoped query with folder, links, recording count, and attachment count includes; ordered pinned-first
  • ddx-api/src/misc/notes/notes-transcription.service.ts:31NotesTranscriptionService.startTranscription — fires NOTE_TRANSCRIPTION_STARTED SSE immediately, runs STT async
  • ddx-api/src/misc/notes/notes-transcription.service.ts:104onTranscriptionComplete — appends transcript to contentText, fires NOTE_TRANSCRIPTION_COMPLETED
  • ddx-api/src/misc/notes/notes-transcription.service.ts:141onOcrComplete — appends OCR full-text from ocrResult.fullText to contentText, fires NOTE_OCR_COMPLETED
  • ddx-api/src/misc/notes/notes.controller.ts:44NotesController@RequireClinicStaff() guard (all clinic roles), @Controller('notes')
  • ddx-api/src/misc/tasks/tasks.service.ts:56TasksService.create — resolves org slug to UUID, creates TaskGroup + items + assignees + links
  • ddx-api/src/misc/activity/activity.service.ts:22ActivityService.trackActivity — UUID guard for service accounts, setImmediate write pattern
  • ddx-api/src/misc/activity/activity.interceptor.ts:12ActivityInterceptor — path regex on GET /patients/:id, post-response tap, setImmediate
  • ddx-api/src/misc/activity/activity.service.ts:68cleanupOldActivity cron — @Cron(EVERY_DAY_AT_2AM), deletes recentActivity older than 90 days
  • ddx-api/prisma-main/models/notes.prisma:26Note model — content Json? (rich-text AST) + contentText String? (plain copy for search), recordings Recording[], attachments Attachment[], links NoteLink[]
  • ddx-api/prisma-main/models/notes.prisma:53NoteLink — polymorphic join on (noteId, entityType, entityId) unique; no FK on entityId (service-layer validated)
  • ddx-api/prisma-main/models/tasks.prisma:50TaskGrouporganizationId, creatorId, category TaskCategory, status TaskStatus, priority TaskPriority, dueDate

Operational Notes

  • Transcription is fire-and-forget: startTranscription returns immediately with a { jobId }. The caller must listen for NOTE_TRANSCRIPTION_COMPLETED or NOTE_TRANSCRIPTION_FAILED SSE events. There is no polling endpoint.
  • Note contentText accumulates: each transcription or OCR result is appended with a separator — it is not replaced. If a note has multiple recordings, the text grows with each --- Transcription --- block. No de-duplication mechanism exists today.
  • Activity interceptor is path-specific: currently wired only to GET /patients/:id. Adding new resource types requires editing activity.interceptor.ts:27 (the path pattern check). The service is generic and accepts any activityType/resourceType string.
  • Task entity links have no FK: TaskLink.entityId is not a database foreign key — integrity is service-layer only. Deleting a linked patient/visit will not cascade-delete TaskLink rows. Validate on read if entity existence matters.
  • 90-day activity retention: cleanupOldActivity cron at EVERY_DAY_AT_2AM is the only housekeeping. There is no per-user manual purge endpoint — only DELETE /activity (clear all for user).
  • Tenant scoping: NotesService always scopes on { organizationId: orgId, userId } — notes are private to the creating user within the org. Tasks use organizationId only (multi-user collaboration).
  • SSE channel: all Note events use SSEChannels.user(userId) — the personal user channel, not an org-level channel. Only the initiating user receives transcription events.
  • Validation commands: pnpm tsc:check (ddx-api). If note/task schema changes: pnpm prisma:generate:all then pnpm prisma migrate dev --schema=prisma-main --name <change>.
  • Messenger — in-platform messaging (comparison: Messenger is multi-party; Notes are single-user scratchpads)
  • SSE Event EngineEventBusService, SSEChannels.user(), event name constants
  • Prisma 7 Schemasprisma-main model file layout; notes.prisma, tasks.prisma, user-activity.prisma
  • Clinical Activities — structured clinical timeline (contrast: Activity Feed here is UI navigation history, not clinical record)