From 934519767a2736661cad6bc23d5eb1e0a049ed08 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Mon, 16 Mar 2026 21:39:22 +0100 Subject: [PATCH] fix: restore obsolete actions in timeline views --- frontend/src/pages/DashboardPage.tsx | 123 ++++- frontend/src/pages/MedicationsPage.tsx | 15 +- frontend/src/pages/SchedulePage.tsx | 93 +++- frontend/src/styles.css | 485 ++++++++++++------- frontend/src/styles/medication-workflows.css | 62 +-- 5 files changed, 539 insertions(+), 239 deletions(-) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 651d3fe..31cdddb 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -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 ( -
+
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 ( -
+
{dose.timeStr} @@ -1251,6 +1277,17 @@ export function DashboardPage() { confirmVariant="warning" /> )} + {showObsoleteConfirm && obsoleteCandidate && ( + 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 ( -
+
)}
+ {isEmpty && med && !med.isObsolete && ( +
+ +
+ )}
{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 ( -
+
{dose.timeStr} @@ -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 ( -
+
)}
+ {isEmpty && med && !med.isObsolete && ( +
+ +
+ )}
{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 ( -
+
{dose.timeStr} diff --git a/frontend/src/pages/MedicationsPage.tsx b/frontend/src/pages/MedicationsPage.tsx index fdf2420..2efc399 100644 --- a/frontend/src/pages/MedicationsPage.tsx +++ b/frontend/src/pages/MedicationsPage.tsx @@ -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() { )} + -
diff --git a/frontend/src/pages/SchedulePage.tsx b/frontend/src/pages/SchedulePage.tsx index 039ac2d..de7f034 100644 --- a/frontend/src/pages/SchedulePage.tsx +++ b/frontend/src/pages/SchedulePage.tsx @@ -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 ( -
+
@@ -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 ( -
+
{dose.timeStr} @@ -517,6 +547,17 @@ export function SchedulePage() { confirmVariant="warning" /> )} + {showObsoleteConfirm && obsoleteCandidate && ( + 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 ( -
+
@@ -553,6 +598,18 @@ export function SchedulePage() { {t(visibleStatus.label)} )}
+ {isEmpty && med && !med.isObsolete && ( +
+ +
+ )}
{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 ( -
+
{dose.timeStr} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 5b46913..8e41dd4 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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); diff --git a/frontend/src/styles/medication-workflows.css b/frontend/src/styles/medication-workflows.css index 67f7aaa..555fe12 100644 --- a/frontend/src/styles/medication-workflows.css +++ b/frontend/src/styles/medication-workflows.css @@ -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; }