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 */
|
/* 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 { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConfirmModal, MedicationAvatar } from "../components";
|
import { ConfirmModal, MedicationAvatar } from "../components";
|
||||||
@@ -85,18 +85,10 @@ export function DashboardPage() {
|
|||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false);
|
const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false);
|
||||||
const [clearingMissed, setClearingMissed] = 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(
|
const isDoseTakenForDisplay = (doseId: string) => takenDoses.has(doseId);
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get structured reminder data
|
// Get structured reminder data
|
||||||
const reminderData = getReminderStatusData(
|
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) =>
|
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined, value: number) =>
|
||||||
isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid"
|
isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid"
|
||||||
? t("form.packageAmountUnitMl")
|
? t("form.packageAmountUnitMl")
|
||||||
@@ -1034,8 +1052,12 @@ export function DashboardPage() {
|
|||||||
? getStockStatus(medCov.daysLeft, medCov.medsLeft, stockThresholds, med?.packageType)
|
? getStockStatus(medCov.daysLeft, medCov.medsLeft, stockThresholds, med?.packageType)
|
||||||
: null;
|
: null;
|
||||||
const status = getVisibleStockStatus(med, rawStatus);
|
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 (
|
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="time-main">
|
||||||
<div className="med-name">
|
<div className="med-name">
|
||||||
<div
|
<div
|
||||||
@@ -1082,8 +1104,12 @@ export function DashboardPage() {
|
|||||||
const allTaken = people.every((person) =>
|
const allTaken = people.every((person) =>
|
||||||
isDoseTakenForDisplay(getDoseId(dose.id, 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 (
|
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-time">{dose.timeStr}</span>
|
||||||
<span className="dose-usage">
|
<span className="dose-usage">
|
||||||
<span className="dose-usage-main">
|
<span className="dose-usage-main">
|
||||||
@@ -1251,6 +1277,17 @@ export function DashboardPage() {
|
|||||||
confirmVariant="warning"
|
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 */}
|
{/* Today - always visible */}
|
||||||
{todayDay &&
|
{todayDay &&
|
||||||
(() => {
|
(() => {
|
||||||
@@ -1329,8 +1366,12 @@ export function DashboardPage() {
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
const visibleStatus = getVisibleStockStatus(med, status);
|
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 (
|
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="time-main">
|
||||||
<div className="med-name">
|
<div className="med-name">
|
||||||
<div
|
<div
|
||||||
@@ -1371,6 +1412,20 @@ export function DashboardPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
<div className="doses-col">
|
<div className="doses-col">
|
||||||
{item.doses.map((dose) => {
|
{item.doses.map((dose) => {
|
||||||
@@ -1379,11 +1434,13 @@ export function DashboardPage() {
|
|||||||
const allTaken = people.every((person) =>
|
const allTaken = people.every((person) =>
|
||||||
isDoseTakenForDisplay(getDoseId(dose.id, 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 (
|
return (
|
||||||
<div
|
<div key={dose.id} className={doseClasses.join(" ")}>
|
||||||
key={dose.id}
|
|
||||||
className={`dose-item ${isOverdue ? "overdue" : ""} ${allTaken ? "all-taken" : ""}`}
|
|
||||||
>
|
|
||||||
<span className="dose-time">{dose.timeStr}</span>
|
<span className="dose-time">{dose.timeStr}</span>
|
||||||
<span className="dose-usage">
|
<span className="dose-usage">
|
||||||
<span className="dose-usage-main">
|
<span className="dose-usage-main">
|
||||||
@@ -1569,7 +1626,7 @@ export function DashboardPage() {
|
|||||||
const medCoverage = coverageByMed[item.medName];
|
const medCoverage = coverageByMed[item.medName];
|
||||||
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
||||||
const depletionTime = depletionByMed[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 willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime;
|
||||||
const status = willBeOutOfStock
|
const status = willBeOutOfStock
|
||||||
? { className: "danger", label: "status.outOfStock" }
|
? { className: "danger", label: "status.outOfStock" }
|
||||||
@@ -1582,8 +1639,12 @@ export function DashboardPage() {
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
const visibleStatus = getVisibleStockStatus(med, status);
|
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 (
|
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="time-main">
|
||||||
<div className="med-name">
|
<div className="med-name">
|
||||||
<div
|
<div
|
||||||
@@ -1624,6 +1685,20 @@ export function DashboardPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
<div className="doses-col">
|
<div className="doses-col">
|
||||||
{item.doses.map((dose) => {
|
{item.doses.map((dose) => {
|
||||||
@@ -1631,8 +1706,12 @@ export function DashboardPage() {
|
|||||||
const allTaken = people.every((person) =>
|
const allTaken = people.every((person) =>
|
||||||
isDoseTakenForDisplay(getDoseId(dose.id, 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 (
|
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-time">{dose.timeStr}</span>
|
||||||
<span className="dose-usage">
|
<span className="dose-usage">
|
||||||
<span className="dose-usage-main">
|
<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/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/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 */
|
/* 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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
@@ -979,6 +979,16 @@ export function MedicationsPage() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<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"
|
className="danger icon-only tooltip-trigger"
|
||||||
onClick={() => requestDeleteMed(med)}
|
onClick={() => requestDeleteMed(med)}
|
||||||
aria-label={t("common.delete")}
|
aria-label={t("common.delete")}
|
||||||
@@ -986,9 +996,6 @@ export function MedicationsPage() {
|
|||||||
>
|
>
|
||||||
<Trash2 size={18} aria-hidden="true" />
|
<Trash2 size={18} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-obsolete" onClick={() => requestMarkObsolete(med)}>
|
|
||||||
{t("medications.list.markObsolete")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="med-details">
|
<div className="med-details">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* biome-ignore-all lint/style/noNestedTernary: schedule timeline branches are intentionally explicit */
|
/* 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 { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConfirmModal, MedicationAvatar } from "../components";
|
import { ConfirmModal, MedicationAvatar } from "../components";
|
||||||
@@ -95,18 +95,10 @@ export function SchedulePage() {
|
|||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false);
|
const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false);
|
||||||
const [clearingMissed, setClearingMissed] = 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(
|
const isDoseTakenForDisplay = (doseId: string) => takenDoses.has(doseId);
|
||||||
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 shouldHideNoScheduleStatusForTube = (
|
const shouldHideNoScheduleStatusForTube = (
|
||||||
med: (typeof meds)[number] | undefined,
|
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) =>
|
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined, value: number) =>
|
||||||
isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid"
|
isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid"
|
||||||
? t("form.packageAmountUnitMl")
|
? t("form.packageAmountUnitMl")
|
||||||
@@ -345,8 +363,16 @@ export function SchedulePage() {
|
|||||||
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
||||||
const medCov = coverageByMed[item.medName];
|
const medCov = coverageByMed[item.medName];
|
||||||
const isEmpty = medCov ? medCov.medsLeft <= 0 : false;
|
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 (
|
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="time-main">
|
||||||
<div className="med-name">
|
<div className="med-name">
|
||||||
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
||||||
@@ -363,8 +389,12 @@ export function SchedulePage() {
|
|||||||
const allTaken = people.every((person) =>
|
const allTaken = people.every((person) =>
|
||||||
isDoseTakenForDisplay(getDoseId(dose.id, 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 (
|
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-time">{dose.timeStr}</span>
|
||||||
<span className="dose-usage">
|
<span className="dose-usage">
|
||||||
<span className="dose-usage-main">
|
<span className="dose-usage-main">
|
||||||
@@ -517,6 +547,17 @@ export function SchedulePage() {
|
|||||||
confirmVariant="warning"
|
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 */}
|
{/* Current and future days */}
|
||||||
{futureDays.map((day) => {
|
{futureDays.map((day) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -540,8 +581,12 @@ export function SchedulePage() {
|
|||||||
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings, med?.packageType)
|
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings, med?.packageType)
|
||||||
: null;
|
: null;
|
||||||
const visibleStatus = shouldHideNoScheduleStatusForTube(med, status) ? null : status;
|
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 (
|
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="time-main">
|
||||||
<div className="med-name">
|
<div className="med-name">
|
||||||
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
|
<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>
|
<span className={`tag ${visibleStatus.className}`}>{t(visibleStatus.label)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
<div className="doses-col">
|
<div className="doses-col">
|
||||||
{item.doses.map((dose) => {
|
{item.doses.map((dose) => {
|
||||||
@@ -561,8 +618,12 @@ export function SchedulePage() {
|
|||||||
const dayStart = new Date(day.date).setHours(0, 0, 0, 0);
|
const dayStart = new Date(day.date).setHours(0, 0, 0, 0);
|
||||||
const isPastDay = dayStart < new 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 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 (
|
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-time">{dose.timeStr}</span>
|
||||||
<span className="dose-usage">
|
<span className="dose-usage">
|
||||||
<span className="dose-usage-main">
|
<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");
|
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -577,25 +578,6 @@ body.modal-open {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
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 {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -732,6 +714,26 @@ body.modal-open {
|
|||||||
background: rgba(0, 0, 0, 0.04);
|
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 {
|
.badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -804,10 +806,6 @@ body.modal-open {
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.med-group-head-toggle:hover .med-group-title {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.med-group-title {
|
.med-group-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
@@ -817,6 +815,10 @@ body.modal-open {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.med-group-head-toggle:hover .med-group-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.med-group-obsolete {
|
.med-group-obsolete {
|
||||||
border-color: var(--border-primary);
|
border-color: var(--border-primary);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
@@ -832,17 +834,6 @@ body.modal-open {
|
|||||||
border-color: var(--border-primary);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.med-grid {
|
.med-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1024,6 +1015,51 @@ body.modal-open {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 0.25rem;
|
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 {
|
.danger-text {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1047,6 +1083,153 @@ body.modal-open {
|
|||||||
font-weight: 400;
|
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 {
|
.med-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1102,6 +1285,8 @@ body.modal-open {
|
|||||||
color: #ffd54a;
|
color: #ffd54a;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border-secondary);
|
border: 1px solid var(--border-secondary);
|
||||||
|
gap: 0.45rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.med-actions button.btn-obsolete:hover {
|
.med-actions button.btn-obsolete:hover {
|
||||||
@@ -1353,152 +1538,43 @@ body.modal-open {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.obsolete-row .med-actions button {
|
||||||
padding: 0.7rem 1.25rem;
|
opacity: 0.72;
|
||||||
border-radius: var(--btn-radius);
|
filter: saturate(0.72);
|
||||||
border: none;
|
box-shadow: 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 {
|
.obsolete-row .med-actions button:hover {
|
||||||
outline: 2px solid var(--accent-light);
|
opacity: 0.9;
|
||||||
outline-offset: 2px;
|
filter: saturate(0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.icon-only {
|
button.timeline-obsolete-btn.btn-obsolete {
|
||||||
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;
|
background: transparent;
|
||||||
border: 1px solid var(--border-secondary);
|
border: 1px solid #f8e38a;
|
||||||
color: var(--text-muted);
|
color: #ffd54a;
|
||||||
box-shadow: none;
|
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.timeline-obsolete-btn.btn-obsolete:hover {
|
||||||
button.btn-nav {
|
background: color-mix(in srgb, #facc15 12%, transparent);
|
||||||
background: var(--bg-secondary);
|
border-color: #ffe27c;
|
||||||
border: 1px solid var(--border-secondary);
|
color: #ffe27c;
|
||||||
color: var(--text-primary);
|
|
||||||
box-shadow: none;
|
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 */
|
[data-theme="light"] button.timeline-obsolete-btn.btn-obsolete {
|
||||||
button.btn-obsolete {
|
background: transparent;
|
||||||
background: var(--btn-obsolete-bg);
|
border-color: #d97706;
|
||||||
border: 1px solid var(--btn-obsolete-border);
|
color: #b45309;
|
||||||
color: var(--btn-obsolete-text);
|
|
||||||
box-shadow: none;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
button.btn-obsolete:hover {
|
|
||||||
background: var(--btn-obsolete-hover);
|
[data-theme="light"] button.timeline-obsolete-btn.btn-obsolete:hover {
|
||||||
transform: none;
|
background: color-mix(in srgb, #f59e0b 10%, transparent);
|
||||||
|
border-color: #b45309;
|
||||||
|
color: #92400e;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
button.btn-obsolete:active {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Danger button (Delete, etc.) */
|
/* Danger button (Delete, etc.) */
|
||||||
button.danger {
|
button.danger {
|
||||||
@@ -2392,6 +2468,42 @@ button.has-validation-error {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
padding-bottom: 0;
|
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 {
|
.time-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2521,6 +2633,47 @@ button.has-validation-error {
|
|||||||
color: var(--text-secondary);
|
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 {
|
.dose-time {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-light);
|
color: var(--accent-light);
|
||||||
|
|||||||
@@ -45,10 +45,41 @@
|
|||||||
gap: 1rem;
|
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 {
|
.refill-footer-right .refill-preview {
|
||||||
height: 42px;
|
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 */
|
/* Refill modal footer mobile */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.refill-modal .modal-footer {
|
.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 toggle row */
|
||||||
.refill-prescription-row {
|
.refill-prescription-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -134,23 +151,6 @@
|
|||||||
margin-left: auto;
|
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 {
|
.refill-section {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user