diff --git a/app/auth/membership-confirmation/page.tsx b/app/auth/membership-confirmation/page.tsx
index f4ae5fe..e93a5ae 100644
--- a/app/auth/membership-confirmation/page.tsx
+++ b/app/auth/membership-confirmation/page.tsx
@@ -5,7 +5,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { withI18n } from '~/lib/i18n/with-i18n';
import {
- PAGE_VIEW_ACTION,
+ PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
@@ -23,7 +23,7 @@ async function MembershipConfirmation() {
}
await createPageViewLog({
accountId: user.id,
- action: PAGE_VIEW_ACTION.REGISTRATION_SUCCESS,
+ action: PageViewAction.REGISTRATION_SUCCESS,
});
return ;
diff --git a/app/doctor/analysis/[id]/page.tsx b/app/doctor/analysis/[id]/page.tsx
index 5a2263c..aac793c 100644
--- a/app/doctor/analysis/[id]/page.tsx
+++ b/app/doctor/analysis/[id]/page.tsx
@@ -3,6 +3,11 @@ import { cache } from 'react';
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
import { PageBody, PageHeader } from '@kit/ui/page';
+import {
+ DoctorPageViewAction,
+ createDoctorPageViewLog,
+} from '~/lib/services/audit/doctorPageView.service';
+
import AnalysisView from '../../_components/analysis-view';
import { DoctorGuard } from '../../_components/doctor-guard';
@@ -20,6 +25,14 @@ async function AnalysisPage({
return null;
}
+ if (analysisResultDetails) {
+ await createDoctorPageViewLog({
+ action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
+ recordKey: id,
+ dataOwnerUserId: analysisResultDetails.patient.userId,
+ });
+ }
+
return (
<>
diff --git a/app/doctor/completed-jobs/page.tsx b/app/doctor/completed-jobs/page.tsx
index 1035fff..1b48f90 100644
--- a/app/doctor/completed-jobs/page.tsx
+++ b/app/doctor/completed-jobs/page.tsx
@@ -1,10 +1,19 @@
+import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
import { PageBody, PageHeader } from '@kit/ui/page';
-import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
-import ResultsTableWrapper from '../_components/results-table-wrapper';
+import {
+ DoctorPageViewAction,
+ createDoctorPageViewLog,
+} from '~/lib/services/audit/doctorPageView.service';
+
import { DoctorGuard } from '../_components/doctor-guard';
+import ResultsTableWrapper from '../_components/results-table-wrapper';
async function CompletedJobsPage() {
+ await createDoctorPageViewLog({
+ action: DoctorPageViewAction.VIEW_DONE_JOBS,
+ });
+
return (
<>
diff --git a/app/doctor/my-jobs/page.tsx b/app/doctor/my-jobs/page.tsx
index 6fdfc78..0f3ad87 100644
--- a/app/doctor/my-jobs/page.tsx
+++ b/app/doctor/my-jobs/page.tsx
@@ -1,9 +1,19 @@
import { getUserInProgressResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
import { PageBody, PageHeader } from '@kit/ui/page';
-import ResultsTableWrapper from '../_components/results-table-wrapper';
+
+import {
+ DoctorPageViewAction,
+ createDoctorPageViewLog,
+} from '~/lib/services/audit/doctorPageView.service';
+
import { DoctorGuard } from '../_components/doctor-guard';
+import ResultsTableWrapper from '../_components/results-table-wrapper';
async function MyReviewsPage() {
+ await createDoctorPageViewLog({
+ action: DoctorPageViewAction.VIEW_OWN_JOBS,
+ });
+
return (
<>
diff --git a/app/doctor/open-jobs/page.tsx b/app/doctor/open-jobs/page.tsx
index b1abf22..9a370c2 100644
--- a/app/doctor/open-jobs/page.tsx
+++ b/app/doctor/open-jobs/page.tsx
@@ -1,10 +1,19 @@
import { getOpenResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
import { PageBody, PageHeader } from '@kit/ui/page';
+import {
+ DoctorPageViewAction,
+ createDoctorPageViewLog,
+} from '~/lib/services/audit/doctorPageView.service';
+
import { DoctorGuard } from '../_components/doctor-guard';
import ResultsTableWrapper from '../_components/results-table-wrapper';
async function OpenJobsPage() {
+ await createDoctorPageViewLog({
+ action: DoctorPageViewAction.VIEW_OPEN_JOBS,
+ });
+
return (
<>
diff --git a/app/doctor/page.tsx b/app/doctor/page.tsx
index 90ca14b..5edc7db 100644
--- a/app/doctor/page.tsx
+++ b/app/doctor/page.tsx
@@ -1,8 +1,18 @@
import { PageBody, PageHeader } from '@kit/ui/page';
+
+import {
+ DoctorPageViewAction,
+ createDoctorPageViewLog,
+} from '~/lib/services/audit/doctorPageView.service';
+
import Dashboard from './_components/doctor-dashboard';
import { DoctorGuard } from './_components/doctor-guard';
async function DoctorPage() {
+ await createDoctorPageViewLog({
+ action: DoctorPageViewAction.VIEW_DASHBOARD,
+ });
+
return (
<>
diff --git a/app/home/(user)/(dashboard)/analysis-results/page.tsx b/app/home/(user)/(dashboard)/analysis-results/page.tsx
index 1ac3d5e..f744b88 100644
--- a/app/home/(user)/(dashboard)/analysis-results/page.tsx
+++ b/app/home/(user)/(dashboard)/analysis-results/page.tsx
@@ -13,7 +13,7 @@ import { pathsConfig } from '@kit/shared/config';
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
import {
- PAGE_VIEW_ACTION,
+ PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
@@ -50,7 +50,7 @@ async function AnalysisResultsPage() {
await createPageViewLog({
accountId: account.id,
- action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS,
+ action: PageViewAction.VIEW_ANALYSIS_RESULTS,
});
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
diff --git a/app/home/(user)/(dashboard)/order-analysis/page.tsx b/app/home/(user)/(dashboard)/order-analysis/page.tsx
index 8e0f179..1736dc9 100644
--- a/app/home/(user)/(dashboard)/order-analysis/page.tsx
+++ b/app/home/(user)/(dashboard)/order-analysis/page.tsx
@@ -6,7 +6,7 @@ import { Trans } from '@kit/ui/trans';
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
import { loadAnalyses } from '../../_lib/server/load-analyses';
import OrderAnalysesCards from '../../_components/order-analyses-cards';
-import { createPageViewLog, PAGE_VIEW_ACTION } from '~/lib/services/audit/pageView.service';
+import { createPageViewLog, PageViewAction } from '~/lib/services/audit/pageView.service';
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
export const generateMetadata = async () => {
@@ -27,7 +27,7 @@ async function OrderAnalysisPage() {
await createPageViewLog({
accountId: account.id,
- action: PAGE_VIEW_ACTION.VIEW_ORDER_ANALYSIS,
+ action: PageViewAction.VIEW_ORDER_ANALYSIS,
});
return (
diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx
index f3b9914..8bc2ade 100644
--- a/app/home/[account]/page.tsx
+++ b/app/home/[account]/page.tsx
@@ -12,7 +12,7 @@ import { PageBody } from '@kit/ui/page';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import {
- PAGE_VIEW_ACTION,
+ PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
@@ -46,7 +46,7 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
use(
createPageViewLog({
accountId: teamAccount.id,
- action: PAGE_VIEW_ACTION.VIEW_TEAM_ACCOUNT_DASHBOARD,
+ action: PageViewAction.VIEW_TEAM_ACCOUNT_DASHBOARD,
}),
);
diff --git a/lib/services/audit/doctorPageView.service.ts b/lib/services/audit/doctorPageView.service.ts
new file mode 100644
index 0000000..36c40a5
--- /dev/null
+++ b/lib/services/audit/doctorPageView.service.ts
@@ -0,0 +1,46 @@
+import { getSupabaseServerClient } from '@kit/supabase/server-client';
+
+export enum DoctorPageViewAction {
+ VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
+ VIEW_DASHBOARD = 'VIEW_DASHBOARD',
+ VIEW_OPEN_JOBS = 'VIEW_OPEN_JOBS',
+ VIEW_OWN_JOBS = 'VIEW_OWN_JOBS',
+ VIEW_DONE_JOBS = 'VIEW_DONE_JOBS',
+}
+
+export const createDoctorPageViewLog = async ({
+ action,
+ recordKey,
+ dataOwnerUserId,
+}: {
+ action: DoctorPageViewAction;
+ recordKey?: string;
+ dataOwnerUserId?: string;
+}) => {
+ try {
+ const supabase = getSupabaseServerClient();
+
+ const {
+ data: { user },
+ error: userError,
+ } = await supabase.auth.getUser();
+
+ if (userError || !user) {
+ console.error('No authenticated user found; skipping audit insert');
+ return;
+ }
+
+ await supabase
+ .schema('audit')
+ .from('doctor_page_views')
+ .insert({
+ viewer_user_id: user.id,
+ data_owner_user_id: dataOwnerUserId,
+ viewed_record_key: recordKey,
+ action,
+ })
+ .throwOnError();
+ } catch (error) {
+ console.error('Failed to insert doctor page view log', error);
+ }
+};
diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts
index 92ce10e..f4e70f6 100644
--- a/lib/services/audit/pageView.service.ts
+++ b/lib/services/audit/pageView.service.ts
@@ -1,6 +1,6 @@
import { getSupabaseServerClient } from '@kit/supabase/server-client';
-export enum PAGE_VIEW_ACTION {
+export enum PageViewAction {
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
@@ -12,7 +12,7 @@ export const createPageViewLog = async ({
action,
}: {
accountId: string;
- action: PAGE_VIEW_ACTION;
+ action: PageViewAction;
}) => {
try {
const supabase = getSupabaseServerClient();
diff --git a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts
index d857aee..47a4aac 100644
--- a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts
+++ b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts
@@ -41,6 +41,7 @@ export const selectJobAction = doctorAction(
} catch (e) {
logger.error('Failed to select job', e);
if (e instanceof Error) {
+ revalidateDoctorAnalysis();
return {
success: false,
reason:
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index 10ce07d..4b3528b 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -42,6 +42,33 @@ export type Database = {
}
Relationships: []
}
+ doctor_page_views: {
+ Row: {
+ action: Database["audit"]["Enums"]["doctor_page_view_action"]
+ created_at: string
+ data_owner_user_id: string | null
+ id: number
+ viewed_record_key: string | null
+ viewer_user_id: string
+ }
+ Insert: {
+ action: Database["audit"]["Enums"]["doctor_page_view_action"]
+ created_at?: string
+ data_owner_user_id?: string | null
+ id?: number
+ viewed_record_key?: string | null
+ viewer_user_id: string
+ }
+ Update: {
+ action?: Database["audit"]["Enums"]["doctor_page_view_action"]
+ created_at?: string
+ data_owner_user_id?: string | null
+ id?: number
+ viewed_record_key?: string | null
+ viewer_user_id?: string
+ }
+ Relationships: []
+ }
log_entries: {
Row: {
changed_at: string
@@ -182,6 +209,12 @@ export type Database = {
[_ in never]: never
}
Enums: {
+ doctor_page_view_action:
+ | "VIEW_ANALYSIS_RESULTS"
+ | "VIEW_DASHBOARD"
+ | "VIEW_OPEN_JOBS"
+ | "VIEW_OWN_JOBS"
+ | "VIEW_DONE_JOBS"
request_status: "SUCCESS" | "FAIL"
sync_status: "SUCCESS" | "FAIL"
}
@@ -1778,7 +1811,6 @@ export type Database = {
primary_owner_user_id: string
name: string
email: string
- personal_code: string
picture_url: string
created_at: string
updated_at: string
@@ -7887,6 +7919,13 @@ export type CompositeTypes<
export const Constants = {
audit: {
Enums: {
+ doctor_page_view_action: [
+ "VIEW_ANALYSIS_RESULTS",
+ "VIEW_DASHBOARD",
+ "VIEW_OPEN_JOBS",
+ "VIEW_OWN_JOBS",
+ "VIEW_DONE_JOBS",
+ ],
request_status: ["SUCCESS", "FAIL"],
sync_status: ["SUCCESS", "FAIL"],
},
diff --git a/supabase/migrations/20250826083609_add_doctor_audit_tables.sql b/supabase/migrations/20250826083609_add_doctor_audit_tables.sql
new file mode 100644
index 0000000..4c1e633
--- /dev/null
+++ b/supabase/migrations/20250826083609_add_doctor_audit_tables.sql
@@ -0,0 +1,85 @@
+
+create type audit.doctor_page_view_action as ENUM('VIEW_ANALYSIS_RESULTS','VIEW_DASHBOARD','VIEW_OPEN_JOBS','VIEW_OWN_JOBS','VIEW_DONE_JOBS');
+
+create table audit.doctor_page_views (
+ "id" bigint generated by default as identity not null,
+ "viewer_user_id" uuid references auth.users (id) not null,
+ "data_owner_user_id" uuid references auth.users (id),
+ "viewed_record_key" text,
+ "action" audit.doctor_page_view_action not null,
+ "created_at" timestamp with time zone not null default now()
+);
+
+grant usage on schema audit to authenticated;
+grant select, insert, update, delete on table audit.doctor_page_views to authenticated;
+
+alter table "audit"."page_views" enable row level security;
+
+create policy "insert_own"
+on audit.doctor_page_views
+as permissive
+for insert
+to authenticated
+with check (auth.uid() = viewer_user_id);
+
+CREATE OR REPLACE FUNCTION medreport.log_doctor_analysis_feedback_changes()
+RETURNS TRIGGER AS $$
+DECLARE
+ current_user_id uuid;
+ current_user_role text;
+ operation_type text;
+BEGIN
+ begin
+ current_user_id := auth.uid();
+ current_user_role := auth.jwt() ->> 'role';
+ end;
+ IF (OLD.doctor_user_id IS DISTINCT FROM NEW.doctor_user_id) THEN
+ operation_type := CASE
+ WHEN NEW.doctor_user_id IS NULL THEN 'UNSELECT_JOB'
+ ELSE 'SELECT_JOB'
+ END;
+ ELSIF (NEW.status = 'DRAFT' OR (OLD.status IS DISTINCT FROM NEW.status AND NEW.status = 'COMPLETED')) THEN
+ operation_type := CASE
+ WHEN NEW.status = 'DRAFT' THEN 'UPDATED_DRAFT'
+ WHEN NEW.status = 'COMPLETED' THEN 'PUBLISHED_SUMMARY'
+ ELSE NULL
+ END;
+ ELSE
+ operation_type := NULL;
+ END IF;
+
+ IF operation_type IS NOT NULL THEN
+ INSERT INTO audit.log_entries (
+ schema_name,
+ table_name,
+ record_key,
+ operation,
+ changed_by,
+ changed_by_role,
+ changed_at
+ )
+ VALUES (
+ 'medreport',
+ 'doctor_analysis_feedback',
+ NEW.id::text,
+ operation_type,
+ current_user_id,
+ current_user_role,
+ NOW()
+ );
+ END IF;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS doctor_analysis_feedback_audit_trigger ON medreport.doctor_analysis_feedback;
+
+CREATE TRIGGER doctor_analysis_feedback_audit_trigger
+ AFTER UPDATE ON medreport.doctor_analysis_feedback
+ FOR EACH ROW
+ EXECUTE FUNCTION medreport.log_doctor_analysis_feedback_changes();
+
+GRANT EXECUTE ON FUNCTION medreport.log_doctor_analysis_feedback_changes() TO authenticated;
+
+alter table audit.doctor_page_views enable row level security;