Skip to content

Commit

Permalink
Structured response view | Symptom, Diagnosis, Allergy (#9703)
Browse files Browse the repository at this point in the history
  • Loading branch information
amjithtitus09 authored Jan 3, 2025
1 parent ccbc92d commit e3daa67
Show file tree
Hide file tree
Showing 8 changed files with 464 additions and 197 deletions.
16 changes: 16 additions & 0 deletions src/Utils/request/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -611,18 +611,34 @@ const routes = {
method: "GET",
TRes: Type<PaginatedResponse<Diagnosis>>(),
},
getDiagnosisById: {
path: "/api/v1/patient/{patientId}/diagnosis/{diagnosisId}/",
method: "GET",
TRes: Type<Diagnosis>(),
},

// Get Symptom
getSymptom: {
path: "/api/v1/patient/{patientId}/symptom/",
method: "GET",
TRes: Type<PaginatedResponse<Symptom>>(),
},
getSymptomById: {
path: "/api/v1/patient/{patientId}/symptom/{symptomId}/",
method: "GET",
TRes: Type<Symptom>(),
},

getAllergy: {
path: "/api/v1/patient/{patientId}/allergy_intolerance/",
method: "GET",
TRes: Type<PaginatedResponse<AllergyIntolerance>>(),
},
getAllergyById: {
path: "/api/v1/patient/{patientId}/allergy_intolerance/{allergyId}/",
method: "GET",
TRes: Type<AllergyIntolerance>(),
},

facilityOrganization: {
list: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Encounter } from "@/types/emr/encounter";
import { Question } from "@/types/questionnaire/question";
import { QuestionnaireResponse } from "@/types/questionnaire/questionnaireResponse";

import { StructuredResponseView } from "./StructuredResponseView";

interface Props {
encounter: Encounter;
}
Expand Down Expand Up @@ -145,7 +147,10 @@ export default function QuestionnaireResponsesList({ encounter }: Props) {
return (
<PaginatedList
route={routes.getQuestionnaireResponses}
pathParams={{ patientId: encounter.patient.id }}
pathParams={{
patientId: encounter.patient.id,
encounterId: encounter.id,
}}
>
{() => (
<div className="mt-4 flex w-full flex-col gap-4">
Expand Down Expand Up @@ -182,29 +187,25 @@ export default function QuestionnaireResponsesList({ encounter }: Props) {
{(item) => (
<Card
key={item.id}
className="flex flex-col p-4 transition-colors hover:bg-muted/50"
className="flex flex-col py-2 px-3 transition-colors hover:bg-muted/50"
>
<div className="flex items-center justify-between">
<div className="flex items-start gap-4">
<CareIcon
icon="l-file-alt"
className="mt-1 h-4 w-4 text-muted-foreground"
/>
<div>
<h3 className="text-base font-medium">
<h3 className="text-sm font-medium">
{item.questionnaire?.title ||
Object.keys(item.structured_responses || {}).map(
(key) => properCase(key),
)}
</h3>
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
<CareIcon icon="l-calender" className="h-3 w-3" />
<CareIcon icon="l-clock" className="h-3 w-3" />
<span>{formatDateTime(item.created_date)}</span>
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
by {item.created_by?.first_name || ""}{" "}
{item.created_by?.last_name || ""}
{` (${item.created_by?.user_type})`}
<span className="mt-0.5 text-xs text-muted-foreground">
by {item.created_by?.first_name || ""}{" "}
{item.created_by?.last_name || ""}
{` (${item.created_by?.user_type})`}
</span>
</div>
</div>
</div>
Expand All @@ -219,38 +220,56 @@ export default function QuestionnaireResponsesList({ encounter }: Props) {

{expandedResponseIds.has(item.id) && (
<div className="mt-3 border-t pt-3">
<div className="space-y-4">
{item.questionnaire?.questions.map(
(question: Question) => {
// Skip structured questions for now as they need special handling
if (question.type === "structured") return null;
{item.questionnaire ? (
// Existing questionnaire response rendering
<div className="space-y-4">
{item.questionnaire?.questions.map(
(question: Question) => {
// Skip structured questions for now as they need special handling
if (question.type === "structured") return null;

const response = item.responses.find(
(r) => r.question_id === question.id,
);
const response = item.responses.find(
(r) => r.question_id === question.id,
);

if (question.type === "group") {
return (
<QuestionGroup
key={question.id}
group={question}
responses={item.responses}
/>
);
}

if (!response) return null;

if (question.type === "group") {
return (
<QuestionGroup
<QuestionResponseValue
key={question.id}
group={question}
responses={item.responses}
question={question}
response={response}
/>
);
}

if (!response) return null;

},
)}
</div>
) : item.structured_responses ? (
// New structured response rendering
Object.entries(item.structured_responses).map(
([type, response]) => {
console.log("LOGGG", type, response);
return (
<QuestionResponseValue
key={question.id}
question={question}
response={response}
<StructuredResponseView
key={response.id}
type={type}
id={response.id}
patientId={encounter.patient.id}
/>
);
},
)}
</div>
)
) : null}
</div>
)}
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useQuery } from "@tanstack/react-query";

import { AllergyTable } from "@/components/Patient/allergy/AllergyTable";
import { DiagnosisTable } from "@/components/Patient/diagnosis/DiagnosisTable";
import { SymptomTable } from "@/components/Patient/symptoms/SymptomTable";

import routes from "@/Utils/request/api";
import query from "@/Utils/request/query";
import { AllergyIntolerance } from "@/types/emr/allergyIntolerance";
import { Diagnosis } from "@/types/questionnaire/diagnosis";
import { Symptom } from "@/types/questionnaire/symptom";

interface Props {
type: string;
id: string;
patientId: string;
}

export function StructuredResponseView({ type, id, patientId }: Props) {
const getRouteAndParams = () => {
const params: Record<string, string> = { patientId };
switch (type) {
case "symptom":
return {
route: routes.getSymptomById,
pathParams: { ...params, symptomId: id },
};
case "diagnosis":
return {
route: routes.getDiagnosisById,
pathParams: { ...params, diagnosisId: id },
};
case "allergy_intolerance":
return {
route: routes.getAllergyById,
pathParams: { ...params, allergyId: id },
};
default:
return null;
}
};

const routeConfig = getRouteAndParams();

const { data, isLoading, error } = useQuery({
queryKey: [type, id],
queryFn: query(routeConfig?.route as any, {
pathParams: routeConfig?.pathParams || { patientId },
}),
enabled: !!id && !!routeConfig,
});

if (!routeConfig) return null;

if (isLoading) {
return <div className="animate-pulse h-20 bg-muted rounded-md" />;
}

if (error) {
console.error(`Error loading ${type}:`, error);
return <div>Error loading {type}</div>;
}

switch (type) {
case "symptom":
return (
<SymptomTable
symptoms={[data as unknown as Symptom]}
showHeader={true}
/>
);
case "diagnosis":
return (
<DiagnosisTable
diagnoses={[data as unknown as Diagnosis]}
showHeader={true}
/>
);
case "allergy_intolerance":
return (
<AllergyTable
allergies={[data as unknown as AllergyIntolerance]}
showHeader={true}
/>
);
default:
return null;
}
}
110 changes: 110 additions & 0 deletions src/components/Patient/allergy/AllergyTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
BeakerIcon,
CookingPotIcon,
HeartPulseIcon,
LeafIcon,
} from "lucide-react";

import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";

import { Avatar } from "@/components/Common/Avatar";

import { AllergyIntolerance } from "@/types/emr/allergyIntolerance";

type AllergyCategory = "food" | "medication" | "environment" | "biologic";

const CATEGORY_ICONS: Record<AllergyCategory, React.ReactNode> = {
food: <CookingPotIcon className="h-4 w-4" />,
medication: <BeakerIcon className="h-4 w-4" />,
environment: <LeafIcon className="h-4 w-4" />,
biologic: <HeartPulseIcon className="h-4 w-4" />,
};

interface AllergyTableProps {
allergies: AllergyIntolerance[];
showHeader?: boolean;
}

export function AllergyTable({
allergies,
showHeader = true,
}: AllergyTableProps) {
return (
<Table>
{showHeader && (
<TableHeader>
<TableRow>
<TableHead className="w-[40px]"></TableHead>
<TableHead>Substance</TableHead>
<TableHead>Status</TableHead>
<TableHead>Critical</TableHead>
<TableHead>Verification</TableHead>
<TableHead>Last Occurrence</TableHead>
<TableHead>By</TableHead>
</TableRow>
</TableHeader>
)}
<TableBody>
{allergies.map((allergy) => (
<TableRow>
<TableCell className="w-[40px]">
{allergy.category &&
CATEGORY_ICONS[allergy.category as AllergyCategory]}
</TableCell>
<TableCell className="font-medium">
{allergy.code.display}
</TableCell>
<TableCell>
<Badge variant="outline" className="capitalize">
{allergy.clinical_status}
</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary" className="capitalize">
{allergy.criticality}
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className="capitalize">
{allergy.verification_status}
</Badge>
</TableCell>
<TableCell className="whitespace-nowrap">
{allergy.last_occurrence
? new Date(allergy.last_occurrence).toLocaleDateString()
: "-"}
</TableCell>
<TableCell className="whitespace-nowrap flex items-center gap-2">
<Avatar
name={`${allergy.created_by?.first_name} ${allergy.created_by?.last_name}`}
className="w-4 h-4"
imageUrl={allergy.created_by?.profile_picture_url}
/>
<span className="text-sm">
{allergy.created_by?.first_name} {allergy.created_by?.last_name}
</span>
</TableCell>
{allergy.note && (
<TableRow>
<TableCell colSpan={7} className="px-4 py-2 bg-muted/50">
<div className="text-xs text-muted-foreground mb-1">
Notes
</div>
<div className="text-sm">{allergy.note}</div>
</TableCell>
</TableRow>
)}
</TableRow>
))}
</TableBody>
</Table>
);
}
Loading

0 comments on commit e3daa67

Please sign in to comment.