feat: improve medication detail modal layout and display (#258)
Widen detail modal on desktop (711px, up from 500px) with max-width override to beat modals-base.css specificity. Limit fullscreen mode to actual phones (<=500px) instead of all screens <=900px. Move intake schedule section before prescription details. Show per-intake takenBy person and bell icon with proper warning color. Right-align time in schedule rows. Move notes icon after label text. Replace emoji bell icons with Lucide Bell component in SchedulePage and MobileEditModal. Add common.on/common.off i18n keys. Closes #254
This commit is contained in:
@@ -626,7 +626,7 @@ export function MedDetailModal({
|
||||
|
||||
<div className="modal-footer">
|
||||
<button className="ghost" onClick={onCloseEditStockModal}>
|
||||
{t("common.cancel")}
|
||||
{t("common.close")}
|
||||
</button>
|
||||
<button className="info" onClick={() => onSubmitStockCorrection(selectedMed.id)} disabled={editStockSaving}>
|
||||
{editStockSaving ? t("editStock.saving") : t("editStock.save")}
|
||||
@@ -803,6 +803,58 @@ export function MedDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Intake Schedule Section */}
|
||||
{selectedMed.blisters.length > 0 && (
|
||||
<div className="med-detail-section">
|
||||
<h3>
|
||||
{t("modal.intakeSchedule")}{" "}
|
||||
{selectedMed.intakeRemindersEnabled && (
|
||||
<span className="reminder-icon info-tooltip" data-tooltip={t("tooltips.intakeReminders")}>
|
||||
<Bell size={14} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="med-detail-schedules">
|
||||
{selectedMed.blisters.map((blister, idx) => {
|
||||
// When using new intakes format with per-intake takenBy,
|
||||
// each intake already represents one person's dose — don't multiply.
|
||||
// For legacy intakes (no per-intake takenBy), multiply by personCount.
|
||||
const intake = selectedMed.intakes?.[idx];
|
||||
const hasPerIntakeTakenBy = !!intake?.takenBy;
|
||||
const personCount = hasPerIntakeTakenBy ? 1 : Math.max(1, selectedMed.takenBy?.length || 1);
|
||||
const totalUsage = blister.usage * personCount;
|
||||
return (
|
||||
<div key={idx} className="med-schedule-item">
|
||||
<span className="med-schedule-usage">
|
||||
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{selectedMed.pillWeightMg &&
|
||||
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
|
||||
</span>
|
||||
<span className="med-schedule-freq">
|
||||
{blister.every === 1 ? t("common.daily") : t("common.everyNDays", { count: blister.every })}
|
||||
</span>
|
||||
{hasPerIntakeTakenBy && intake.takenBy && (
|
||||
<span className="med-schedule-person">{intake.takenBy}</span>
|
||||
)}
|
||||
{intake?.intakeRemindersEnabled && (
|
||||
<span className="med-schedule-bell" role="img" aria-label={t("tooltips.intakeReminders")}>
|
||||
<Bell size={13} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<span className="med-schedule-time">
|
||||
{t("modal.at")}{" "}
|
||||
{new Date(blister.start).toLocaleTimeString(getSystemLocale(i18n.language), {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prescription Details Section */}
|
||||
{selectedMed.prescriptionEnabled && (
|
||||
<div className="med-detail-section">
|
||||
@@ -839,50 +891,6 @@ export function MedDetailModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Intake Schedule Section */}
|
||||
{selectedMed.blisters.length > 0 && (
|
||||
<div className="med-detail-section">
|
||||
<h3>
|
||||
{t("modal.intakeSchedule")}{" "}
|
||||
{selectedMed.intakeRemindersEnabled && (
|
||||
<span className="reminder-icon info-tooltip" data-tooltip={t("tooltips.intakeReminders")}>
|
||||
<Bell size={14} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="med-detail-schedules">
|
||||
{selectedMed.blisters.map((blister, idx) => {
|
||||
// When using new intakes format with per-intake takenBy,
|
||||
// each intake already represents one person's dose — don't multiply.
|
||||
// For legacy intakes (no per-intake takenBy), multiply by personCount.
|
||||
const intake = selectedMed.intakes?.[idx];
|
||||
const hasPerIntakeTakenBy = !!intake?.takenBy;
|
||||
const personCount = hasPerIntakeTakenBy ? 1 : Math.max(1, selectedMed.takenBy?.length || 1);
|
||||
const totalUsage = blister.usage * personCount;
|
||||
return (
|
||||
<div key={idx} className="med-schedule-item">
|
||||
<span className="med-schedule-usage">
|
||||
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{selectedMed.pillWeightMg &&
|
||||
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
|
||||
</span>
|
||||
<span className="med-schedule-freq">
|
||||
{blister.every === 1 ? t("common.daily") : t("common.everyNDays", { count: blister.every })}
|
||||
</span>
|
||||
<span className="med-schedule-time">
|
||||
{t("modal.at")}{" "}
|
||||
{new Date(blister.start).toLocaleTimeString(getSystemLocale(i18n.language), {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coverage Status Section */}
|
||||
{medCoverage && status && (
|
||||
<div className="med-detail-section">
|
||||
@@ -909,10 +917,10 @@ export function MedDetailModal({
|
||||
{selectedMed.notes && (
|
||||
<div className="med-detail-section">
|
||||
<h3>
|
||||
{t("modal.notes")}{" "}
|
||||
<span className="notes-icon notes-icon-static" aria-hidden="true">
|
||||
<NotebookPen size={14} />
|
||||
</span>{" "}
|
||||
{t("modal.notes")}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="med-notes-content">{selectedMed.notes}</div>
|
||||
</div>
|
||||
@@ -1111,7 +1119,7 @@ export function MedDetailModal({
|
||||
|
||||
<div className="modal-footer">
|
||||
<button className="ghost" onClick={onCloseRefillModal}>
|
||||
{t("common.cancel")}
|
||||
{t("common.close")}
|
||||
</button>
|
||||
<div className="refill-footer-right">
|
||||
<button
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
},
|
||||
"form": {
|
||||
"editEntry": "Bearbeiten",
|
||||
"editEntryWithName": "Bearbeiten: {{name}}",
|
||||
"viewEntry": "Ansehen",
|
||||
"newEntry": "Neues Medikament",
|
||||
"badge": "Packungen + lose Tabletten",
|
||||
@@ -462,7 +463,9 @@
|
||||
"pillsTotal": "{{count}} Tabletten gesamt",
|
||||
"pillsTotal_one": "{{count}} Tablette gesamt",
|
||||
"pillsTotal_other": "{{count}} Tabletten gesamt",
|
||||
"max": "max"
|
||||
"max": "max",
|
||||
"on": "An",
|
||||
"off": "Aus"
|
||||
},
|
||||
"share": {
|
||||
"button": "Teilen",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
},
|
||||
"form": {
|
||||
"editEntry": "Edit",
|
||||
"editEntryWithName": "Edit: {{name}}",
|
||||
"viewEntry": "View",
|
||||
"newEntry": "New medication",
|
||||
"badge": "Packs + loose pills",
|
||||
@@ -462,7 +463,9 @@
|
||||
"pillsTotal": "{{count}} pills total",
|
||||
"pillsTotal_one": "{{count}} pill total",
|
||||
"pillsTotal_other": "{{count}} pills total",
|
||||
"max": "max"
|
||||
"max": "max",
|
||||
"on": "On",
|
||||
"off": "Off"
|
||||
},
|
||||
"share": {
|
||||
"button": "Share",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Bell } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MedicationAvatar } from "../components";
|
||||
import { useAuth } from "../components/Auth";
|
||||
@@ -204,7 +205,7 @@ export function SchedulePage() {
|
||||
className="reminder-icon info-tooltip"
|
||||
data-tooltip={t("tooltips.intakeReminders")}
|
||||
>
|
||||
🔔
|
||||
<Bell size={14} aria-hidden="true" />
|
||||
</span>
|
||||
)}{" "}
|
||||
<div className="dose-checks">
|
||||
@@ -365,7 +366,7 @@ export function SchedulePage() {
|
||||
className="reminder-icon info-tooltip"
|
||||
data-tooltip={t("tooltips.intakeReminders")}
|
||||
>
|
||||
🔔
|
||||
<Bell size={14} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<div className="dose-checks">
|
||||
|
||||
+21
-4
@@ -4323,9 +4323,10 @@ button.has-validation-error {
|
||||
/* Modal base styles moved to styles/modals-base.css */
|
||||
|
||||
/* Medication Detail Modal */
|
||||
.med-detail-modal {
|
||||
.modal-content.med-detail-modal {
|
||||
padding: 0;
|
||||
width: min(100vw - 1rem, 520px);
|
||||
width: min(100vw - 1rem, 711px);
|
||||
max-width: 711px;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-primary);
|
||||
overscroll-behavior: contain;
|
||||
@@ -4668,6 +4669,22 @@ button.has-validation-error {
|
||||
|
||||
.med-schedule-time {
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.med-schedule-person {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.med-schedule-bell {
|
||||
color: var(--warning);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-theme="light"] .med-schedule-bell {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.med-detail-footer {
|
||||
@@ -4689,7 +4706,7 @@ button.has-validation-error {
|
||||
|
||||
/* Mobile devices can report wide CSS viewports (e.g., 768px in device emulation).
|
||||
Use input modality instead of width-only breakpoints so the modal still fills the handset viewport. */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
@media (hover: none) and (pointer: coarse) and (max-width: 500px) {
|
||||
.med-detail-overlay {
|
||||
padding: 0.4rem;
|
||||
align-items: stretch;
|
||||
@@ -4938,7 +4955,7 @@ button.has-validation-error {
|
||||
}
|
||||
|
||||
/* Hard mobile override for MedDetailModal: remove side frame and use full handset viewport. */
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 500px) {
|
||||
.modal-overlay.med-detail-overlay {
|
||||
padding: 0 !important;
|
||||
align-items: stretch;
|
||||
|
||||
Reference in New Issue
Block a user