fix: restore obsolete actions in timeline views
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/* biome-ignore-all lint/style/noNestedTernary: timeline rendering uses explicit UI-state branching */
|
||||
import { Bell, ClipboardList, NotebookPen, Share2 } from "lucide-react";
|
||||
import { Archive, Bell, ClipboardList, NotebookPen, Share2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmModal, MedicationAvatar } from "../components";
|
||||
@@ -85,18 +85,10 @@ export function DashboardPage() {
|
||||
} = useAppContext();
|
||||
const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false);
|
||||
const [clearingMissed, setClearingMissed] = useState(false);
|
||||
const [showObsoleteConfirm, setShowObsoleteConfirm] = useState(false);
|
||||
const [obsoleteCandidate, setObsoleteCandidate] = useState<{ id: number; name: string } | null>(null);
|
||||
|
||||
const outOfStockMedicationIds = new Set(
|
||||
meds.filter((med) => (coverageByMed[getMedDisplayName(med)]?.medsLeft ?? 1) <= 0).map((med) => med.id)
|
||||
);
|
||||
|
||||
const isDoseTakenForDisplay = (doseId: string) => {
|
||||
const medId = Number.parseInt(doseId.split("-")[0] ?? "", 10);
|
||||
if (!Number.isNaN(medId) && outOfStockMedicationIds.has(medId)) {
|
||||
return false;
|
||||
}
|
||||
return takenDoses.has(doseId);
|
||||
};
|
||||
const isDoseTakenForDisplay = (doseId: string) => takenDoses.has(doseId);
|
||||
|
||||
// Get structured reminder data
|
||||
const reminderData = getReminderStatusData(
|
||||
@@ -210,6 +202,32 @@ export function DashboardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const requestMarkObsolete = (med: { id: number; name: string }) => {
|
||||
setObsoleteCandidate(med);
|
||||
setShowObsoleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleConfirmMarkObsolete = async () => {
|
||||
if (!obsoleteCandidate) return;
|
||||
try {
|
||||
const res = await fetch(`/api/medications/${obsoleteCandidate.id}/obsolete`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
await loadMeds();
|
||||
setShowObsoleteConfirm(false);
|
||||
setObsoleteCandidate(null);
|
||||
} catch {
|
||||
alert(t("common.saveFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelMarkObsolete = () => {
|
||||
setShowObsoleteConfirm(false);
|
||||
setObsoleteCandidate(null);
|
||||
};
|
||||
|
||||
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined, value: number) =>
|
||||
isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid"
|
||||
? t("form.packageAmountUnitMl")
|
||||
@@ -1034,8 +1052,12 @@ export function DashboardPage() {
|
||||
? getStockStatus(medCov.daysLeft, medCov.medsLeft, stockThresholds, med?.packageType)
|
||||
: null;
|
||||
const status = getVisibleStockStatus(med, rawStatus);
|
||||
const isLowStock = !isEmpty && status?.className === "warning";
|
||||
const rowClasses = ["time-row"];
|
||||
if (isEmpty) rowClasses.push("med-empty");
|
||||
else if (isLowStock) rowClasses.push("med-low");
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className="time-row">
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={rowClasses.join(" ")}>
|
||||
<div className="time-main">
|
||||
<div className="med-name">
|
||||
<div
|
||||
@@ -1082,8 +1104,12 @@ export function DashboardPage() {
|
||||
const allTaken = people.every((person) =>
|
||||
isDoseTakenForDisplay(getDoseId(dose.id, person))
|
||||
);
|
||||
const doseClasses = ["dose-item", "past"];
|
||||
if (allTaken) doseClasses.push("all-taken");
|
||||
if (isEmpty) doseClasses.push("med-empty");
|
||||
else if (isLowStock) doseClasses.push("med-low");
|
||||
return (
|
||||
<div key={dose.id} className={`dose-item past ${allTaken ? "all-taken" : ""}`}>
|
||||
<div key={dose.id} className={doseClasses.join(" ")}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
@@ -1251,6 +1277,17 @@ export function DashboardPage() {
|
||||
confirmVariant="warning"
|
||||
/>
|
||||
)}
|
||||
{showObsoleteConfirm && obsoleteCandidate && (
|
||||
<ConfirmModal
|
||||
title={t("medications.obsoleteModal.title")}
|
||||
message={t("medications.obsoleteModal.message", { name: obsoleteCandidate.name })}
|
||||
confirmLabel={t("medications.list.markObsolete")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={() => void handleConfirmMarkObsolete()}
|
||||
onCancel={handleCancelMarkObsolete}
|
||||
confirmVariant="warning"
|
||||
/>
|
||||
)}
|
||||
{/* Today - always visible */}
|
||||
{todayDay &&
|
||||
(() => {
|
||||
@@ -1329,8 +1366,12 @@ export function DashboardPage() {
|
||||
)
|
||||
: null;
|
||||
const visibleStatus = getVisibleStockStatus(med, status);
|
||||
const isLowStock = !isEmpty && visibleStatus?.className === "warning";
|
||||
const rowClasses = ["time-row"];
|
||||
if (isEmpty) rowClasses.push("med-empty");
|
||||
else if (isLowStock) rowClasses.push("med-low");
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className="time-row">
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={rowClasses.join(" ")}>
|
||||
<div className="time-main">
|
||||
<div className="med-name">
|
||||
<div
|
||||
@@ -1371,6 +1412,20 @@ export function DashboardPage() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isEmpty && med && !med.isObsolete && (
|
||||
<div className="timeline-obsolete-row">
|
||||
<button
|
||||
type="button"
|
||||
className="timeline-obsolete-btn btn-obsolete"
|
||||
onClick={() =>
|
||||
requestMarkObsolete({ id: med.id, name: getMedDisplayName(med) })
|
||||
}
|
||||
>
|
||||
<Archive size={16} aria-hidden="true" />
|
||||
<span>{t("medications.list.markObsolete")}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
{item.doses.map((dose) => {
|
||||
@@ -1379,11 +1434,13 @@ export function DashboardPage() {
|
||||
const allTaken = people.every((person) =>
|
||||
isDoseTakenForDisplay(getDoseId(dose.id, person))
|
||||
);
|
||||
const doseClasses = ["dose-item"];
|
||||
if (isOverdue) doseClasses.push("overdue");
|
||||
if (allTaken) doseClasses.push("all-taken");
|
||||
if (isEmpty) doseClasses.push("med-empty");
|
||||
else if (isLowStock) doseClasses.push("med-low");
|
||||
return (
|
||||
<div
|
||||
key={dose.id}
|
||||
className={`dose-item ${isOverdue ? "overdue" : ""} ${allTaken ? "all-taken" : ""}`}
|
||||
>
|
||||
<div key={dose.id} className={doseClasses.join(" ")}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
@@ -1569,7 +1626,7 @@ export function DashboardPage() {
|
||||
const medCoverage = coverageByMed[item.medName];
|
||||
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
||||
const depletionTime = depletionByMed[item.medName];
|
||||
const _isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false;
|
||||
const isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false;
|
||||
const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime;
|
||||
const status = willBeOutOfStock
|
||||
? { className: "danger", label: "status.outOfStock" }
|
||||
@@ -1582,8 +1639,12 @@ export function DashboardPage() {
|
||||
)
|
||||
: null;
|
||||
const visibleStatus = getVisibleStockStatus(med, status);
|
||||
const isLowStock = !isEmpty && visibleStatus?.className === "warning";
|
||||
const rowClasses = ["time-row"];
|
||||
if (isEmpty) rowClasses.push("med-empty");
|
||||
else if (isLowStock) rowClasses.push("med-low");
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className="time-row">
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={rowClasses.join(" ")}>
|
||||
<div className="time-main">
|
||||
<div className="med-name">
|
||||
<div
|
||||
@@ -1624,6 +1685,20 @@ export function DashboardPage() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isEmpty && med && !med.isObsolete && (
|
||||
<div className="timeline-obsolete-row">
|
||||
<button
|
||||
type="button"
|
||||
className="timeline-obsolete-btn btn-obsolete"
|
||||
onClick={() =>
|
||||
requestMarkObsolete({ id: med.id, name: getMedDisplayName(med) })
|
||||
}
|
||||
>
|
||||
<Archive size={16} aria-hidden="true" />
|
||||
<span>{t("medications.list.markObsolete")}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
{item.doses.map((dose) => {
|
||||
@@ -1631,8 +1706,12 @@ export function DashboardPage() {
|
||||
const allTaken = people.every((person) =>
|
||||
isDoseTakenForDisplay(getDoseId(dose.id, person))
|
||||
);
|
||||
const doseClasses = ["dose-item", "future"];
|
||||
if (allTaken) doseClasses.push("all-taken");
|
||||
if (isEmpty) doseClasses.push("med-empty");
|
||||
else if (isLowStock) doseClasses.push("med-low");
|
||||
return (
|
||||
<div key={dose.id} className={`dose-item future ${allTaken ? "all-taken" : ""}`}>
|
||||
<div key={dose.id} className={doseClasses.join(" ")}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* biome-ignore-all lint/a11y/noLabelWithoutControl: form uses custom inputs and display fields wrapped in label-like layout */
|
||||
/* biome-ignore-all lint/correctness/useExhaustiveDependencies: modal-history callbacks are intentionally managed outside hook deps */
|
||||
/* biome-ignore-all lint/suspicious/noArrayIndexKey: local draft intake rows do not have stable ids before persistence */
|
||||
import { Bell, Eye, Minus, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { Archive, Bell, Eye, Minus, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
@@ -979,6 +979,16 @@ export function MedicationsPage() {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-obsolete"
|
||||
onClick={() => requestMarkObsolete(med)}
|
||||
aria-label={t("medications.list.markObsolete")}
|
||||
>
|
||||
<Archive size={16} aria-hidden="true" />
|
||||
<span>{t("medications.list.markObsolete")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="danger icon-only tooltip-trigger"
|
||||
onClick={() => requestDeleteMed(med)}
|
||||
aria-label={t("common.delete")}
|
||||
@@ -986,9 +996,6 @@ export function MedicationsPage() {
|
||||
>
|
||||
<Trash2 size={18} aria-hidden="true" />
|
||||
</button>
|
||||
<button className="btn-obsolete" onClick={() => requestMarkObsolete(med)}>
|
||||
{t("medications.list.markObsolete")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="med-details">
|
||||
<span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* biome-ignore-all lint/style/noNestedTernary: schedule timeline branches are intentionally explicit */
|
||||
import { Bell } from "lucide-react";
|
||||
import { Archive, Bell } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmModal, MedicationAvatar } from "../components";
|
||||
@@ -95,18 +95,10 @@ export function SchedulePage() {
|
||||
} = useAppContext();
|
||||
const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false);
|
||||
const [clearingMissed, setClearingMissed] = useState(false);
|
||||
const [showObsoleteConfirm, setShowObsoleteConfirm] = useState(false);
|
||||
const [obsoleteCandidate, setObsoleteCandidate] = useState<{ id: number; name: string } | null>(null);
|
||||
|
||||
const outOfStockMedicationIds = new Set(
|
||||
meds.filter((med) => (coverageByMed[getMedDisplayName(med)]?.medsLeft ?? 1) <= 0).map((med) => med.id)
|
||||
);
|
||||
|
||||
const isDoseTakenForDisplay = (doseId: string) => {
|
||||
const medId = Number.parseInt(doseId.split("-")[0] ?? "", 10);
|
||||
if (!Number.isNaN(medId) && outOfStockMedicationIds.has(medId)) {
|
||||
return false;
|
||||
}
|
||||
return takenDoses.has(doseId);
|
||||
};
|
||||
const isDoseTakenForDisplay = (doseId: string) => takenDoses.has(doseId);
|
||||
|
||||
const shouldHideNoScheduleStatusForTube = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
@@ -174,6 +166,32 @@ export function SchedulePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const requestMarkObsolete = (med: { id: number; name: string }) => {
|
||||
setObsoleteCandidate(med);
|
||||
setShowObsoleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleConfirmMarkObsolete = async () => {
|
||||
if (!obsoleteCandidate) return;
|
||||
try {
|
||||
const res = await fetch(`/api/medications/${obsoleteCandidate.id}/obsolete`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
await loadMeds();
|
||||
setShowObsoleteConfirm(false);
|
||||
setObsoleteCandidate(null);
|
||||
} catch {
|
||||
alert(t("common.saveFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelMarkObsolete = () => {
|
||||
setShowObsoleteConfirm(false);
|
||||
setObsoleteCandidate(null);
|
||||
};
|
||||
|
||||
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined, value: number) =>
|
||||
isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid"
|
||||
? t("form.packageAmountUnitMl")
|
||||
@@ -345,8 +363,16 @@ export function SchedulePage() {
|
||||
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
||||
const medCov = coverageByMed[item.medName];
|
||||
const isEmpty = medCov ? medCov.medsLeft <= 0 : false;
|
||||
const rawStatus = medCov
|
||||
? getStockStatus(medCov.daysLeft, medCov.medsLeft, settings, med?.packageType)
|
||||
: null;
|
||||
const visibleStatus = shouldHideNoScheduleStatusForTube(med, rawStatus) ? null : rawStatus;
|
||||
const isLowStock = !isEmpty && visibleStatus?.className === "warning";
|
||||
const rowClasses = ["time-row"];
|
||||
if (isEmpty) rowClasses.push("med-empty");
|
||||
else if (isLowStock) rowClasses.push("med-low");
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className="time-row">
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={rowClasses.join(" ")}>
|
||||
<div className="time-main">
|
||||
<div className="med-name">
|
||||
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
||||
@@ -363,8 +389,12 @@ export function SchedulePage() {
|
||||
const allTaken = people.every((person) =>
|
||||
isDoseTakenForDisplay(getDoseId(dose.id, person))
|
||||
);
|
||||
const doseClasses = ["dose-item", "past"];
|
||||
if (allTaken) doseClasses.push("all-taken");
|
||||
if (isEmpty) doseClasses.push("med-empty");
|
||||
else if (isLowStock) doseClasses.push("med-low");
|
||||
return (
|
||||
<div key={dose.id} className={`dose-item past ${allTaken ? "all-taken" : ""}`}>
|
||||
<div key={dose.id} className={doseClasses.join(" ")}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
@@ -517,6 +547,17 @@ export function SchedulePage() {
|
||||
confirmVariant="warning"
|
||||
/>
|
||||
)}
|
||||
{showObsoleteConfirm && obsoleteCandidate && (
|
||||
<ConfirmModal
|
||||
title={t("medications.obsoleteModal.title")}
|
||||
message={t("medications.obsoleteModal.message", { name: obsoleteCandidate.name })}
|
||||
confirmLabel={t("medications.list.markObsolete")}
|
||||
cancelLabel={t("common.cancel")}
|
||||
onConfirm={() => void handleConfirmMarkObsolete()}
|
||||
onCancel={handleCancelMarkObsolete}
|
||||
confirmVariant="warning"
|
||||
/>
|
||||
)}
|
||||
{/* Current and future days */}
|
||||
{futureDays.map((day) => {
|
||||
const today = new Date();
|
||||
@@ -540,8 +581,12 @@ export function SchedulePage() {
|
||||
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings, med?.packageType)
|
||||
: null;
|
||||
const visibleStatus = shouldHideNoScheduleStatusForTube(med, status) ? null : status;
|
||||
const isLowStock = !isEmpty && visibleStatus?.className === "warning";
|
||||
const rowClasses = ["time-row"];
|
||||
if (isEmpty) rowClasses.push("med-empty");
|
||||
else if (isLowStock) rowClasses.push("med-low");
|
||||
return (
|
||||
<div key={`${day.dateStr}-${item.medName}`} className="time-row">
|
||||
<div key={`${day.dateStr}-${item.medName}`} className={rowClasses.join(" ")}>
|
||||
<div className="time-main">
|
||||
<div className="med-name">
|
||||
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
||||
@@ -553,6 +598,18 @@ export function SchedulePage() {
|
||||
<span className={`tag ${visibleStatus.className}`}>{t(visibleStatus.label)}</span>
|
||||
)}
|
||||
</div>
|
||||
{isEmpty && med && !med.isObsolete && (
|
||||
<div className="timeline-obsolete-row">
|
||||
<button
|
||||
type="button"
|
||||
className="timeline-obsolete-btn btn-obsolete"
|
||||
onClick={() => requestMarkObsolete({ id: med.id, name: getMedDisplayName(med) })}
|
||||
>
|
||||
<Archive size={16} aria-hidden="true" />
|
||||
<span>{t("medications.list.markObsolete")}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
{item.doses.map((dose) => {
|
||||
@@ -561,8 +618,12 @@ export function SchedulePage() {
|
||||
const dayStart = new Date(day.date).setHours(0, 0, 0, 0);
|
||||
const isPastDay = dayStart < new Date().setHours(0, 0, 0, 0);
|
||||
const allTaken = people.every((person) => isDoseTakenForDisplay(getDoseId(dose.id, person)));
|
||||
const doseClasses = ["dose-item"];
|
||||
if (allTaken) doseClasses.push("all-taken");
|
||||
if (isEmpty) doseClasses.push("med-empty");
|
||||
else if (isLowStock) doseClasses.push("med-low");
|
||||
return (
|
||||
<div key={dose.id} className={`dose-item ${allTaken ? "all-taken" : ""}`}>
|
||||
<div key={dose.id} className={doseClasses.join(" ")}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
|
||||
+319
-166
@@ -1,3 +1,4 @@
|
||||
/* biome-ignore-all lint/style/noDescendingSpecificity: legacy shared stylesheet relies on intentional cascade ordering across base and contextual selectors */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap");
|
||||
|
||||
:root {
|
||||
@@ -577,25 +578,6 @@ body.modal-open {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tabs .pill {
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 150ms ease,
|
||||
border-color 150ms ease;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-secondary);
|
||||
color: var(--text-muted);
|
||||
box-shadow: none;
|
||||
}
|
||||
.tabs .pill:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.tabs .pill.primary {
|
||||
background: var(--accent-bg);
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
@@ -732,6 +714,26 @@ body.modal-open {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.tabs .pill {
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 150ms ease,
|
||||
border-color 150ms ease;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-secondary);
|
||||
color: var(--text-muted);
|
||||
box-shadow: none;
|
||||
}
|
||||
.tabs .pill:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.tabs .pill.primary {
|
||||
background: var(--accent-bg);
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.badges {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -804,10 +806,6 @@ body.modal-open {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.med-group-head-toggle:hover .med-group-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.med-group-title {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
@@ -817,6 +815,10 @@ body.modal-open {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.med-group-head-toggle:hover .med-group-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.med-group-obsolete {
|
||||
border-color: var(--border-primary);
|
||||
background: var(--bg-secondary);
|
||||
@@ -832,17 +834,6 @@ body.modal-open {
|
||||
border-color: var(--border-primary);
|
||||
}
|
||||
|
||||
.obsolete-row .med-actions button {
|
||||
opacity: 0.72;
|
||||
filter: saturate(0.72);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.obsolete-row .med-actions button:hover {
|
||||
opacity: 0.9;
|
||||
filter: saturate(0.85);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.med-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1024,6 +1015,51 @@ body.modal-open {
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.timeline-obsolete-row {
|
||||
margin-top: 0.15rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.timeline-obsolete-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
min-height: 2.5rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
border: 1px solid #f8e38a;
|
||||
color: #ffd54a;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.timeline-obsolete-btn:hover {
|
||||
background: color-mix(in srgb, #facc15 12%, transparent);
|
||||
border-color: #ffe27c;
|
||||
color: #ffe27c;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.timeline-obsolete-btn svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-theme="light"] .timeline-obsolete-btn {
|
||||
background: transparent;
|
||||
border-color: #d97706;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
[data-theme="light"] .timeline-obsolete-btn:hover {
|
||||
background: color-mix(in srgb, #f59e0b 10%, transparent);
|
||||
border-color: #b45309;
|
||||
color: #92400e;
|
||||
}
|
||||
.danger-text {
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
@@ -1047,6 +1083,153 @@ body.modal-open {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.7rem 1.25rem;
|
||||
border-radius: var(--btn-radius);
|
||||
border: none;
|
||||
background: var(--btn-primary-bg);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: var(--btn-shadow);
|
||||
transition:
|
||||
background 150ms ease,
|
||||
box-shadow 150ms ease,
|
||||
opacity 150ms ease;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--btn-primary-hover);
|
||||
}
|
||||
button:active {
|
||||
box-shadow: var(--btn-shadow);
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--accent-light);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button.icon-only {
|
||||
min-width: 2.75rem;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button.icon-only svg,
|
||||
.modal-close svg,
|
||||
.btn-copy svg,
|
||||
.share-btn svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
stroke-width: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Secondary button (Edit, etc.) */
|
||||
button.secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
}
|
||||
button.secondary:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
[data-theme="light"] button.secondary {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
[data-theme="light"] button.secondary:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Success button (Refill, etc.) */
|
||||
button.success {
|
||||
background: var(--success);
|
||||
color: var(--btn-success-text);
|
||||
border: none;
|
||||
}
|
||||
button.success:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
button.success:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Primary/Accent button (New entry, Add intake, etc.) */
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.primary:hover {
|
||||
background: var(--accent-light);
|
||||
}
|
||||
button.primary:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Info button (Edit, secondary actions) */
|
||||
button.info {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.info:hover {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
/* Ghost button (Cancel, etc.) */
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-secondary);
|
||||
color: var(--text-muted);
|
||||
box-shadow: none;
|
||||
}
|
||||
button.ghost:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
}
|
||||
[data-theme="light"] button.ghost:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
}
|
||||
|
||||
/* Navigation button (Back): neutral and low visual urgency */
|
||||
button.btn-nav {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
color: var(--text-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
button.btn-nav:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Reversible status-change button (Mark obsolete): warning, not destructive */
|
||||
button.btn-obsolete {
|
||||
background: var(--btn-obsolete-bg);
|
||||
border: 1px solid var(--btn-obsolete-border);
|
||||
color: var(--btn-obsolete-text);
|
||||
box-shadow: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
button.btn-obsolete:hover {
|
||||
background: var(--btn-obsolete-hover);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
button.btn-obsolete:active {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.med-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1102,6 +1285,8 @@ body.modal-open {
|
||||
color: #ffd54a;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-secondary);
|
||||
gap: 0.45rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.med-actions button.btn-obsolete:hover {
|
||||
@@ -1353,152 +1538,43 @@ body.modal-open {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.7rem 1.25rem;
|
||||
border-radius: var(--btn-radius);
|
||||
border: none;
|
||||
background: var(--btn-primary-bg);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: var(--btn-shadow);
|
||||
transition:
|
||||
background 150ms ease,
|
||||
box-shadow 150ms ease,
|
||||
opacity 150ms ease;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--btn-primary-hover);
|
||||
}
|
||||
button:active {
|
||||
box-shadow: var(--btn-shadow);
|
||||
.obsolete-row .med-actions button {
|
||||
opacity: 0.72;
|
||||
filter: saturate(0.72);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--accent-light);
|
||||
outline-offset: 2px;
|
||||
.obsolete-row .med-actions button:hover {
|
||||
opacity: 0.9;
|
||||
filter: saturate(0.85);
|
||||
}
|
||||
|
||||
button.icon-only {
|
||||
min-width: 2.75rem;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button.icon-only svg,
|
||||
.modal-close svg,
|
||||
.btn-copy svg,
|
||||
.share-btn svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
stroke-width: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Secondary button (Edit, etc.) */
|
||||
button.secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
}
|
||||
button.secondary:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
[data-theme="light"] button.secondary {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
[data-theme="light"] button.secondary:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Success button (Refill, etc.) */
|
||||
button.success {
|
||||
background: var(--success);
|
||||
color: var(--btn-success-text);
|
||||
border: none;
|
||||
}
|
||||
button.success:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
button.success:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Primary/Accent button (New entry, Add intake, etc.) */
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.primary:hover {
|
||||
background: var(--accent-light);
|
||||
}
|
||||
button.primary:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Info button (Edit, secondary actions) */
|
||||
button.info {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button.info:hover {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
/* Ghost button (Cancel, etc.) */
|
||||
button.ghost {
|
||||
button.timeline-obsolete-btn.btn-obsolete {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-secondary);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid #f8e38a;
|
||||
color: #ffd54a;
|
||||
box-shadow: none;
|
||||
}
|
||||
button.ghost:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
}
|
||||
[data-theme="light"] button.ghost:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
}
|
||||
|
||||
/* Navigation button (Back): neutral and low visual urgency */
|
||||
button.btn-nav {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-secondary);
|
||||
color: var(--text-primary);
|
||||
button.timeline-obsolete-btn.btn-obsolete:hover {
|
||||
background: color-mix(in srgb, #facc15 12%, transparent);
|
||||
border-color: #ffe27c;
|
||||
color: #ffe27c;
|
||||
box-shadow: none;
|
||||
}
|
||||
button.btn-nav:hover {
|
||||
background: var(--btn-ghost-hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Reversible status-change button (Mark obsolete): warning, not destructive */
|
||||
button.btn-obsolete {
|
||||
background: var(--btn-obsolete-bg);
|
||||
border: 1px solid var(--btn-obsolete-border);
|
||||
color: var(--btn-obsolete-text);
|
||||
box-shadow: none;
|
||||
font-weight: 700;
|
||||
[data-theme="light"] button.timeline-obsolete-btn.btn-obsolete {
|
||||
background: transparent;
|
||||
border-color: #d97706;
|
||||
color: #b45309;
|
||||
}
|
||||
button.btn-obsolete:hover {
|
||||
background: var(--btn-obsolete-hover);
|
||||
transform: none;
|
||||
|
||||
[data-theme="light"] button.timeline-obsolete-btn.btn-obsolete:hover {
|
||||
background: color-mix(in srgb, #f59e0b 10%, transparent);
|
||||
border-color: #b45309;
|
||||
color: #92400e;
|
||||
box-shadow: none;
|
||||
}
|
||||
button.btn-obsolete:active {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Danger button (Delete, etc.) */
|
||||
button.danger {
|
||||
@@ -2392,6 +2468,42 @@ button.has-validation-error {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.time-row.med-empty {
|
||||
border-left: 3px solid var(--danger);
|
||||
padding-left: 0.6rem;
|
||||
background: color-mix(in srgb, var(--danger) 8%, transparent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.time-row.med-low {
|
||||
border-left: 3px solid var(--warning);
|
||||
padding-left: 0.6rem;
|
||||
background: color-mix(in srgb, var(--warning) 10%, transparent);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.time-row.med-low .med-name-text {
|
||||
color: color-mix(in srgb, var(--warning) 88%, white 12%);
|
||||
}
|
||||
|
||||
.time-row.med-low .tag.subtle {
|
||||
background: color-mix(in srgb, var(--warning) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--warning) 42%, transparent);
|
||||
color: color-mix(in srgb, var(--warning) 82%, white 18%);
|
||||
}
|
||||
|
||||
.time-row.med-empty .med-name-text {
|
||||
color: var(--danger);
|
||||
text-decoration: line-through;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
.time-row.med-empty .tag.subtle {
|
||||
background: color-mix(in srgb, var(--danger) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--danger) 42%, transparent);
|
||||
color: color-mix(in srgb, var(--danger) 82%, white 18%);
|
||||
}
|
||||
.time-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2521,6 +2633,47 @@ button.has-validation-error {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dose-item.med-low,
|
||||
.dose-item.med-low.all-taken,
|
||||
.dose-item.med-low.taken,
|
||||
.dose-item.med-low.future,
|
||||
.dose-item.med-low.overdue,
|
||||
.dose-item.med-low.overdue.taken {
|
||||
border-color: rgba(252, 211, 77, 0.5);
|
||||
box-shadow: inset 3px 0 0 color-mix(in srgb, var(--warning) 88%, black 12%);
|
||||
}
|
||||
|
||||
.dose-item.med-low:not(.all-taken):not(.taken):not(.overdue):not(.med-empty) {
|
||||
background: color-mix(in srgb, var(--warning) 9%, var(--accent-bg));
|
||||
}
|
||||
|
||||
.dose-item.med-empty,
|
||||
.dose-item.med-empty.all-taken,
|
||||
.dose-item.med-empty.taken,
|
||||
.dose-item.med-empty.future,
|
||||
.dose-item.med-empty.overdue,
|
||||
.dose-item.med-empty.overdue.taken {
|
||||
background: color-mix(in srgb, var(--danger) 13%, transparent);
|
||||
border-color: color-mix(in srgb, var(--danger) 46%, transparent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dose-item.med-empty .dose-time,
|
||||
.dose-item.med-empty .dose-usage {
|
||||
color: color-mix(in srgb, var(--danger) 82%, white 18%);
|
||||
text-decoration: line-through;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
.dose-item.med-empty .dose-person {
|
||||
background: color-mix(in srgb, var(--danger) 18%, transparent);
|
||||
}
|
||||
|
||||
.dose-item.med-empty .dose-person .person-name,
|
||||
.dose-item.med-empty .dose-person.taken .person-name {
|
||||
color: color-mix(in srgb, var(--danger) 80%, white 20%);
|
||||
}
|
||||
|
||||
.dose-time {
|
||||
font-weight: 600;
|
||||
color: var(--accent-light);
|
||||
|
||||
@@ -45,10 +45,41 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.refill-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
padding: 0 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--success);
|
||||
border-radius: 6px;
|
||||
color: var(--success);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.refill-footer-right .refill-preview {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
/* Refill: submit row (button + pill preview) */
|
||||
.refill-submit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.refill-submit-row button {
|
||||
height: 42px;
|
||||
padding: 0 2rem;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Refill modal footer mobile */
|
||||
@media (max-width: 640px) {
|
||||
.refill-modal .modal-footer {
|
||||
@@ -70,20 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Refill: submit row (button + pill preview) */
|
||||
.refill-submit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.refill-submit-row button {
|
||||
height: 42px;
|
||||
padding: 0 2rem;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Refill: prescription toggle row */
|
||||
.refill-prescription-row {
|
||||
display: flex;
|
||||
@@ -134,23 +151,6 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.refill-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
padding: 0 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--success);
|
||||
border-radius: 6px;
|
||||
color: var(--success);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.refill-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user