Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions components/MedicalReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,74 @@ const getStyles = (isRtl: boolean) => {
textAlign: textStart,
width: '100%',
},
pricingSection: {
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: BRAND.slate100,
},
pricingLabel: {
fontSize: 8.5,
fontFamily: f(true),
color: '#047857',
marginBottom: 6,
textAlign: textStart,
letterSpacing: isRtl ? 0 : 0.6,
textTransform: labelTransform,
width: '100%',
},
priceBox: {
backgroundColor: '#ecfdf5',
borderWidth: 1,
borderColor: '#a7f3d0',
borderRadius: 10,
paddingVertical: 8,
paddingHorizontal: 12,
marginBottom: 8,
},
priceText: {
fontSize: 14,
fontFamily: f(true),
color: '#065f46',
textAlign: textStart,
},
altRow: {
flexDirection: isRtl ? 'row-reverse' : 'row',
alignItems: 'flex-start',
marginBottom: 6,
padding: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: BRAND.slate100,
backgroundColor: BRAND.slate50,
},
altName: {
fontSize: 10,
fontFamily: f(true),
color: BRAND.slate900,
textAlign: textStart,
},
altDetail: {
fontSize: 8.5,
fontFamily: f(false),
color: BRAND.slate500,
textAlign: textStart,
marginTop: 2,
},
altPrice: {
fontSize: 9,
fontFamily: f(true),
color: '#047857',
textAlign: textStart,
marginTop: 2,
},
pricingDisclaimer: {
fontSize: 7.5,
fontFamily: f(false),
color: BRAND.slate400,
marginTop: 6,
textAlign: textStart,
},
interactionBox: {
marginBottom: 12,
padding: 16,
Expand Down Expand Up @@ -447,12 +515,26 @@ const getStyles = (isRtl: boolean) => {
});
};

interface PriceEstimatePDF {
price: number;
currency: string;
}

interface EgyptianAlternativePDF {
name: string;
manufacturer: string;
estimatedPrice: PriceEstimatePDF;
note: string;
}

interface Medication {
name: string;
dosage: string;
usage: string;
tip: string;
reminders: { time: string; label: string }[];
estimatedPrice?: PriceEstimatePDF;
egyptianAlternatives?: EgyptianAlternativePDF[];
}
Comment on lines +518 to 538

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid redeclaring pricing types — import the canonical ones.

PriceEstimatePDF and EgyptianAlternativePDF are structurally identical to PriceEstimate and EgyptianAlternative in lib/types/prescription.ts, and the local Medication interface here is a structural subset of the exported Medication. Redeclaring them invites drift (e.g., if note becomes optional in the canonical type, the PDF copy will still mark it required). Reuse the shared types so changes there propagate automatically.

Proposed fix
-import { normalizeInteractionSeverity, type InteractionSeverity } from '@/lib/types/prescription';
+import {
+  normalizeInteractionSeverity,
+  type InteractionSeverity,
+  type Medication,
+  type Interaction,
+  type PriceEstimate,
+  type EgyptianAlternative,
+} from '@/lib/types/prescription';
@@
-interface PriceEstimatePDF {
-  price: number;
-  currency: string;
-}
-
-interface EgyptianAlternativePDF {
-  name: string;
-  manufacturer: string;
-  estimatedPrice: PriceEstimatePDF;
-  note: string;
-}
-
-interface Medication {
-  name: string;
-  dosage: string;
-  usage: string;
-  tip: string;
-  reminders: { time: string; label: string }[];
-  estimatedPrice?: PriceEstimatePDF;
-  egyptianAlternatives?: EgyptianAlternativePDF[];
-}
-
-interface Interaction {
-  severity: string;
-  description: string;
-}

Note: the unsafe access on line 715 (alt.estimatedPrice.currency} {alt.estimatedPrice.price) is part of the broader null-safety issue raised on lib/types/prescription.ts and will need a guard once the canonical type marks estimatedPrice optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/MedicalReport.tsx` around lines 518 - 538, Replace the locally
redeclared types PriceEstimatePDF, EgyptianAlternativePDF and Medication with
the canonical exports from lib/types/prescription.ts (import PriceEstimate,
EgyptianAlternative and Medication and use them instead of the local
declarations) to avoid type drift; update the interface references in this file
to use those imported symbols, remove the duplicate declarations, and add a
null-safety guard around accesses like alt.estimatedPrice.currency /
alt.estimatedPrice.price (or narrow via optional chaining) since estimatedPrice
may be optional in the shared type.


interface Interaction {
Expand Down Expand Up @@ -484,6 +566,10 @@ interface MedicalReportProps {
severityDisplayMedium: string;
severityDisplayLow: string;
summaryLabel: string;
pricingSection: string;
estimatedPriceLabel: string;
alternativesLabel: string;
pricingDisclaimer: string;
};
}

Expand Down Expand Up @@ -600,6 +686,43 @@ export const MedicalReport = ({
</View>
</>
) : null}

{/* Pricing & Alternatives Section */}
{(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (
<View style={styles.pricingSection}>
<Text style={styles.pricingLabel}>{labels.pricingSection}</Text>

{med.estimatedPrice && med.estimatedPrice.price > 0 ? (
<View style={styles.priceBox}>
<Text style={styles.priceText}>
{labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price}
</Text>
</View>
) : null}

{med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? (
<View style={{ marginTop: 4 }}>
<Text style={[styles.pricingLabel, { marginTop: 4, marginBottom: 8 }]}>{labels.alternativesLabel}</Text>
{med.egyptianAlternatives.map((alt, ai) => (
<View key={ai} style={styles.altRow}>
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}>
<Text style={styles.altName}>{alt.name}</Text>
<Text style={styles.altDetail}>{alt.manufacturer}</Text>
{alt.note ? <Text style={styles.altDetail}>{alt.note}</Text> : null}
</View>
<View style={{ flexShrink: 0, alignItems: isRtl ? 'flex-start' : 'flex-end' }}>
<Text style={styles.altPrice}>
{alt.estimatedPrice.currency} {alt.estimatedPrice.price}
</Text>
</View>
</View>
))}
</View>
) : null}

<Text style={styles.pricingDisclaimer}>{labels.pricingDisclaimer}</Text>
</View>
) : null}
Comment on lines +689 to +725

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pricing section can render with no content.

The outer guard med.estimatedPrice || (med.egyptianAlternatives?.length > 0) admits the case where med.estimatedPrice is present with price === 0 and egyptianAlternatives is empty. In that scenario both inner blocks short-circuit (line 695 requires price > 0; line 703 requires non-empty alternatives) and the section ends up rendering only the section title and disclaimer. Tighten the outer guard to match:

Proposed fix
-              {(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (
+              {((med.estimatedPrice && med.estimatedPrice.price > 0) || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Pricing & Alternatives Section */}
{(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (
<View style={styles.pricingSection}>
<Text style={styles.pricingLabel}>{labels.pricingSection}</Text>
{med.estimatedPrice && med.estimatedPrice.price > 0 ? (
<View style={styles.priceBox}>
<Text style={styles.priceText}>
{labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price}
</Text>
</View>
) : null}
{med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? (
<View style={{ marginTop: 4 }}>
<Text style={[styles.pricingLabel, { marginTop: 4, marginBottom: 8 }]}>{labels.alternativesLabel}</Text>
{med.egyptianAlternatives.map((alt, ai) => (
<View key={ai} style={styles.altRow}>
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}>
<Text style={styles.altName}>{alt.name}</Text>
<Text style={styles.altDetail}>{alt.manufacturer}</Text>
{alt.note ? <Text style={styles.altDetail}>{alt.note}</Text> : null}
</View>
<View style={{ flexShrink: 0, alignItems: isRtl ? 'flex-start' : 'flex-end' }}>
<Text style={styles.altPrice}>
{alt.estimatedPrice.currency} {alt.estimatedPrice.price}
</Text>
</View>
</View>
))}
</View>
) : null}
<Text style={styles.pricingDisclaimer}>{labels.pricingDisclaimer}</Text>
</View>
) : null}
{/* Pricing & Alternatives Section */}
{((med.estimatedPrice && med.estimatedPrice.price > 0) || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (
<View style={styles.pricingSection}>
<Text style={styles.pricingLabel}>{labels.pricingSection}</Text>
{med.estimatedPrice && med.estimatedPrice.price > 0 ? (
<View style={styles.priceBox}>
<Text style={styles.priceText}>
{labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price}
</Text>
</View>
) : null}
{med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? (
<View style={{ marginTop: 4 }}>
<Text style={[styles.pricingLabel, { marginTop: 4, marginBottom: 8 }]}>{labels.alternativesLabel}</Text>
{med.egyptianAlternatives.map((alt, ai) => (
<View key={ai} style={styles.altRow}>
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}>
<Text style={styles.altName}>{alt.name}</Text>
<Text style={styles.altDetail}>{alt.manufacturer}</Text>
{alt.note ? <Text style={styles.altDetail}>{alt.note}</Text> : null}
</View>
<View style={{ flexShrink: 0, alignItems: isRtl ? 'flex-start' : 'flex-end' }}>
<Text style={styles.altPrice}>
{alt.estimatedPrice.currency} {alt.estimatedPrice.price}
</Text>
</View>
</View>
))}
</View>
) : null}
<Text style={styles.pricingDisclaimer}>{labels.pricingDisclaimer}</Text>
</View>
) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/MedicalReport.tsx` around lines 689 - 725, The pricing section
currently checks (med.estimatedPrice || (med.egyptianAlternatives &&
med.egyptianAlternatives.length > 0)) which still renders when
estimatedPrice.price === 0 and alternatives are empty; change the outer guard to
require the same detailed conditions used inside: only render if
(med.estimatedPrice && med.estimatedPrice.price > 0) ||
(med.egyptianAlternatives && med.egyptianAlternatives.length > 0), so the inner
blocks (the estimated price block that checks med.estimatedPrice.price > 0 and
the egyptianAlternatives.map block) are guaranteed to produce content before
showing the pricingSection, keeping labels.pricingSection and pricingDisclaimer
from rendering alone.

</View>
))}

Expand Down
14 changes: 14 additions & 0 deletions components/results/MedicationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AlertTriangle, Clock, Stethoscope, ExternalLink } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl';
import { formatTime12h } from '@/lib/format-time';
import type { Medication } from '@/lib/types/prescription';
import { MedicationPricing } from './MedicationPricing';

type MedicationCardProps = {
medication: Medication;
Expand Down Expand Up @@ -40,6 +41,13 @@ export function MedicationCard({ medication, index }: MedicationCardProps) {
<Clock className="h-4 w-4 shrink-0" aria-hidden />
<span>{medication.dosage}</span>
</div>

{/* Inline price badge */}
{medication.estimatedPrice && medication.estimatedPrice.price > 0 && (
<span className="inline-flex items-center gap-1 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200/60 dark:border-emerald-700/30 px-2 py-0.5 text-[11px] font-black tabular-nums text-emerald-700 dark:text-emerald-300">
💊 {medication.estimatedPrice.currency} {medication.estimatedPrice.price}
</span>
)}

<a
href={`https://www.google.com/search?q=${encodeURIComponent(medication.name + ' medication uses')}`}
Expand Down Expand Up @@ -105,6 +113,12 @@ export function MedicationCard({ medication, index }: MedicationCardProps) {
</div>
</div>
) : null}

<MedicationPricing
estimatedPrice={medication.estimatedPrice}
egyptianAlternatives={medication.egyptianAlternatives}
medicationName={medication.name}
/>
</div>
</div>
</motion.article>
Expand Down
Loading