Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33c1095e77 |
@@ -0,0 +1,117 @@
|
|||||||
|
import { Minus, Plus } from "lucide-react";
|
||||||
|
|
||||||
|
interface FormNumberStepperProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (nextValue: string) => void;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
allowDecimal?: boolean;
|
||||||
|
decrementLabel: string;
|
||||||
|
incrementLabel: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DECIMAL_ROUNDING_FACTOR = 1000;
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max?: number): number {
|
||||||
|
const clampedMin = Math.max(min, value);
|
||||||
|
if (max == null) return clampedMin;
|
||||||
|
return Math.min(max, clampedMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDecimal(value: number): number {
|
||||||
|
return Math.round(value * DECIMAL_ROUNDING_FACTOR) / DECIMAL_ROUNDING_FACTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDisplayValue(value: number, allowDecimal: boolean): string {
|
||||||
|
if (!allowDecimal) return String(Math.max(0, Math.trunc(value)));
|
||||||
|
const normalized = normalizeDecimal(value);
|
||||||
|
return normalized.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRawInput(raw: string, allowDecimal: boolean): string {
|
||||||
|
const normalizedRaw = raw.replace(",", ".");
|
||||||
|
if (allowDecimal) {
|
||||||
|
const cleaned = normalizedRaw.replace(/[^\d.]/g, "");
|
||||||
|
const [integerPart = "", ...fractionalParts] = cleaned.split(".");
|
||||||
|
if (fractionalParts.length === 0) return integerPart;
|
||||||
|
return `${integerPart}.${fractionalParts.join("")}`;
|
||||||
|
}
|
||||||
|
return normalizedRaw.replace(/\D/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInputValue(raw: string, allowDecimal: boolean): number | null {
|
||||||
|
if (raw.trim() === "") return null;
|
||||||
|
const parsed = allowDecimal ? Number.parseFloat(raw) : Number.parseInt(raw, 10);
|
||||||
|
if (Number.isNaN(parsed)) return null;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormNumberStepper({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
min = 0,
|
||||||
|
max,
|
||||||
|
step = 1,
|
||||||
|
allowDecimal = false,
|
||||||
|
decrementLabel,
|
||||||
|
incrementLabel,
|
||||||
|
className = "",
|
||||||
|
}: FormNumberStepperProps) {
|
||||||
|
const parsed = parseInputValue(value, allowDecimal);
|
||||||
|
const baseValue = parsed ?? min;
|
||||||
|
const canDecrement = baseValue > min;
|
||||||
|
const canIncrement = max == null || baseValue < max;
|
||||||
|
|
||||||
|
const normalizedClassName = ["number-stepper", "form-number-stepper", className].filter(Boolean).join(" ");
|
||||||
|
|
||||||
|
const handleStep = (direction: -1 | 1) => {
|
||||||
|
const nextRaw = clamp(baseValue + direction * step, min, max);
|
||||||
|
onChange(toDisplayValue(nextRaw, allowDecimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (nextRaw: string) => {
|
||||||
|
onChange(sanitizeRawInput(nextRaw, allowDecimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
const nextParsed = parseInputValue(value, allowDecimal);
|
||||||
|
if (nextParsed == null) return;
|
||||||
|
const clamped = clamp(nextParsed, min, max);
|
||||||
|
onChange(toDisplayValue(clamped, allowDecimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={normalizedClassName}>
|
||||||
|
{/* Input first in DOM so <label> associates with it, not the decrement button.
|
||||||
|
CSS order restores the visual layout: [−] [input] [+]. */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode={allowDecimal ? "decimal" : "numeric"}
|
||||||
|
pattern={allowDecimal ? "[0-9]*\\.?[0-9]*" : "[0-9]*"}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => handleInputChange(e.target.value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="stepper-btn decrement"
|
||||||
|
onClick={() => handleStep(-1)}
|
||||||
|
disabled={!canDecrement}
|
||||||
|
aria-label={decrementLabel}
|
||||||
|
>
|
||||||
|
<Minus size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="stepper-btn increment"
|
||||||
|
onClick={() => handleStep(1)}
|
||||||
|
disabled={!canIncrement}
|
||||||
|
aria-label={incrementLabel}
|
||||||
|
>
|
||||||
|
<Plus size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import type { DoseUnit, FieldErrors, FormBlister, FormIntake, FormState, Medicat
|
|||||||
import { DOSE_UNITS } from "../types";
|
import { DOSE_UNITS } from "../types";
|
||||||
import { deriveTotal } from "../utils";
|
import { deriveTotal } from "../utils";
|
||||||
import { DateInput } from "./DateInput";
|
import { DateInput } from "./DateInput";
|
||||||
|
import { FormNumberStepper } from "./FormNumberStepper";
|
||||||
|
|
||||||
// Field limits for validation
|
// Field limits for validation
|
||||||
const FIELD_LIMITS = {
|
const FIELD_LIMITS = {
|
||||||
@@ -107,6 +108,8 @@ export function MobileEditModal({
|
|||||||
onSaveMedication,
|
onSaveMedication,
|
||||||
}: MobileEditModalProps) {
|
}: MobileEditModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const decrementValueLabel = t("editStock.decreaseValue");
|
||||||
|
const incrementValueLabel = t("editStock.increaseValue");
|
||||||
const [activeTab, setActiveTab] = useState<MobileTab>("general");
|
const [activeTab, setActiveTab] = useState<MobileTab>("general");
|
||||||
const fieldsetRef = useRef<HTMLFieldSetElement | null>(null);
|
const fieldsetRef = useRef<HTMLFieldSetElement | null>(null);
|
||||||
const tabStripRef = useRef<HTMLDivElement | null>(null);
|
const tabStripRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -499,32 +502,32 @@ export function MobileEditModal({
|
|||||||
<>
|
<>
|
||||||
<label>
|
<label>
|
||||||
{t("form.packs")}
|
{t("form.packs")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.packCount}
|
value={form.packCount}
|
||||||
onChange={(e) => onHandleValueChange("packCount", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("packCount", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.blistersPerPack")}
|
{t("form.blistersPerPack")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.blistersPerPack}
|
value={form.blistersPerPack}
|
||||||
onChange={(e) => onHandleValueChange("blistersPerPack", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("blistersPerPack", nextValue)}
|
||||||
|
min={1}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.pillsPerBlister")}
|
{t("form.pillsPerBlister")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.pillsPerBlister}
|
value={form.pillsPerBlister}
|
||||||
onChange={(e) => onHandleValueChange("pillsPerBlister", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("pillsPerBlister", nextValue)}
|
||||||
|
min={1}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -536,22 +539,22 @@ export function MobileEditModal({
|
|||||||
<>
|
<>
|
||||||
<label>
|
<label>
|
||||||
{t("form.totalCapacity")}
|
{t("form.totalCapacity")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.totalPills}
|
value={form.totalPills}
|
||||||
onChange={(e) => onHandleValueChange("totalPills", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("totalPills", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.currentPills")}
|
{t("form.currentPills")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.looseTablets}
|
value={form.looseTablets}
|
||||||
onChange={(e) => onHandleValueChange("looseTablets", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("looseTablets", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
@@ -646,22 +649,24 @@ export function MobileEditModal({
|
|||||||
>
|
>
|
||||||
<label className="compact">
|
<label className="compact">
|
||||||
<span>{t("form.blisters.usage")}</span>
|
<span>{t("form.blisters.usage")}</span>
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
pattern="[0-9]*\.?[0-9]*"
|
|
||||||
value={intake.usage}
|
value={intake.usage}
|
||||||
onChange={(e) => onSetIntakeValue(idx, "usage", e.target.value)}
|
onChange={(nextValue) => onSetIntakeValue(idx, "usage", nextValue)}
|
||||||
|
min={0.5}
|
||||||
|
step={0.5}
|
||||||
|
allowDecimal={true}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="compact">
|
<label className="compact">
|
||||||
<span>{t("form.blisters.everyDays")}</span>
|
<span>{t("form.blisters.everyDays")}</span>
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={intake.every}
|
value={intake.every}
|
||||||
onChange={(e) => onSetIntakeValue(idx, "every", e.target.value)}
|
onChange={(nextValue) => onSetIntakeValue(idx, "every", nextValue)}
|
||||||
|
min={1}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="compact full-row">
|
<label className="compact full-row">
|
||||||
@@ -740,32 +745,32 @@ export function MobileEditModal({
|
|||||||
<>
|
<>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
{t("prescription.authorizedRefills")}
|
{t("prescription.authorizedRefills")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.prescriptionAuthorizedRefills}
|
value={form.prescriptionAuthorizedRefills}
|
||||||
onChange={(e) => onHandleValueChange("prescriptionAuthorizedRefills", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("prescriptionAuthorizedRefills", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
{t("prescription.remainingRefills")}
|
{t("prescription.remainingRefills")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.prescriptionRemainingRefills}
|
value={form.prescriptionRemainingRefills}
|
||||||
onChange={(e) => onHandleValueChange("prescriptionRemainingRefills", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("prescriptionRemainingRefills", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
{t("prescription.lowThreshold")}
|
{t("prescription.lowThreshold")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.prescriptionLowRefillThreshold}
|
value={form.prescriptionLowRefillThreshold}
|
||||||
onChange={(e) => onHandleValueChange("prescriptionLowRefillThreshold", e.target.value)}
|
onChange={(nextValue) => onHandleValueChange("prescriptionLowRefillThreshold", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export { ConfirmModal } from "./ConfirmModal";
|
|||||||
export { DateInput } from "./DateInput";
|
export { DateInput } from "./DateInput";
|
||||||
export { DateTimeInput } from "./DateTimeInput";
|
export { DateTimeInput } from "./DateTimeInput";
|
||||||
export { default as ExportModal } from "./ExportModal";
|
export { default as ExportModal } from "./ExportModal";
|
||||||
|
export { FormNumberStepper } from "./FormNumberStepper";
|
||||||
export type { LightboxProps } from "./Lightbox";
|
export type { LightboxProps } from "./Lightbox";
|
||||||
|
|
||||||
export { Lightbox } from "./Lightbox";
|
export { Lightbox } from "./Lightbox";
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import { 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";
|
||||||
import { ConfirmModal, DateInput, Lightbox, MedicationAvatar, MobileEditModal, ReportModal } from "../components";
|
import {
|
||||||
|
ConfirmModal,
|
||||||
|
DateInput,
|
||||||
|
FormNumberStepper,
|
||||||
|
Lightbox,
|
||||||
|
MedicationAvatar,
|
||||||
|
MobileEditModal,
|
||||||
|
ReportModal,
|
||||||
|
} from "../components";
|
||||||
import { useAuth } from "../components/Auth";
|
import { useAuth } from "../components/Auth";
|
||||||
import { useAppContext, useUnsavedChanges } from "../context";
|
import { useAppContext, useUnsavedChanges } from "../context";
|
||||||
import { useMedicationForm, useModalHistory, useUnsavedChangesWarning } from "../hooks";
|
import { useMedicationForm, useModalHistory, useUnsavedChangesWarning } from "../hooks";
|
||||||
@@ -175,6 +183,8 @@ export function MedicationsPage() {
|
|||||||
const pillsPerBlister = Number(form.pillsPerBlister) || 1;
|
const pillsPerBlister = Number(form.pillsPerBlister) || 1;
|
||||||
return packCount * blistersPerPack * pillsPerBlister;
|
return packCount * blistersPerPack * pillsPerBlister;
|
||||||
}, [form.packageType, form.packCount, form.blistersPerPack, form.pillsPerBlister, form.looseTablets]);
|
}, [form.packageType, form.packCount, form.blistersPerPack, form.pillsPerBlister, form.looseTablets]);
|
||||||
|
const decrementValueLabel = t("editStock.decreaseValue");
|
||||||
|
const incrementValueLabel = t("editStock.increaseValue");
|
||||||
|
|
||||||
const dateConsistencyError = useMemo(() => {
|
const dateConsistencyError = useMemo(() => {
|
||||||
const medicationStartDate = form.medicationStartDate;
|
const medicationStartDate = form.medicationStartDate;
|
||||||
@@ -966,15 +976,6 @@ export function MedicationsPage() {
|
|||||||
>
|
>
|
||||||
{t("form.sections.stock")}
|
{t("form.sections.stock")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={activeTab === "prescription"}
|
|
||||||
className={`form-tab${activeTab === "prescription" ? " active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("prescription")}
|
|
||||||
>
|
|
||||||
{t("form.sections.prescription")}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
@@ -984,6 +985,15 @@ export function MedicationsPage() {
|
|||||||
>
|
>
|
||||||
{t("form.sections.schedule")}
|
{t("form.sections.schedule")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === "prescription"}
|
||||||
|
className={`form-tab${activeTab === "prescription" ? " active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("prescription")}
|
||||||
|
>
|
||||||
|
{t("form.sections.prescription")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<fieldset className="readonly-fieldset" disabled={readOnlyView}>
|
<fieldset className="readonly-fieldset" disabled={readOnlyView}>
|
||||||
<div className={`form-tab-panel${activeTab === "general" ? " active" : ""}`}>
|
<div className={`form-tab-panel${activeTab === "general" ? " active" : ""}`}>
|
||||||
@@ -1157,32 +1167,32 @@ export function MedicationsPage() {
|
|||||||
<>
|
<>
|
||||||
<label>
|
<label>
|
||||||
{t("form.packs")}
|
{t("form.packs")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.packCount}
|
value={form.packCount}
|
||||||
onChange={(e) => handleValueChange("packCount", e.target.value)}
|
onChange={(nextValue) => handleValueChange("packCount", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.blistersPerPack")}
|
{t("form.blistersPerPack")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.blistersPerPack}
|
value={form.blistersPerPack}
|
||||||
onChange={(e) => handleValueChange("blistersPerPack", e.target.value)}
|
onChange={(nextValue) => handleValueChange("blistersPerPack", nextValue)}
|
||||||
|
min={1}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.pillsPerBlister")}
|
{t("form.pillsPerBlister")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.pillsPerBlister}
|
value={form.pillsPerBlister}
|
||||||
onChange={(e) => handleValueChange("pillsPerBlister", e.target.value)}
|
onChange={(nextValue) => handleValueChange("pillsPerBlister", nextValue)}
|
||||||
|
min={1}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -1194,22 +1204,22 @@ export function MedicationsPage() {
|
|||||||
<>
|
<>
|
||||||
<label>
|
<label>
|
||||||
{t("form.totalCapacity")}
|
{t("form.totalCapacity")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.totalPills}
|
value={form.totalPills}
|
||||||
onChange={(e) => handleValueChange("totalPills", e.target.value)}
|
onChange={(nextValue) => handleValueChange("totalPills", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.currentPills")}
|
{t("form.currentPills")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.looseTablets}
|
value={form.looseTablets}
|
||||||
onChange={(e) => handleValueChange("looseTablets", e.target.value)}
|
onChange={(nextValue) => handleValueChange("looseTablets", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
@@ -1300,32 +1310,32 @@ export function MedicationsPage() {
|
|||||||
<>
|
<>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
{t("prescription.authorizedRefills")}
|
{t("prescription.authorizedRefills")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.prescriptionAuthorizedRefills}
|
value={form.prescriptionAuthorizedRefills}
|
||||||
onChange={(e) => handleValueChange("prescriptionAuthorizedRefills", e.target.value)}
|
onChange={(nextValue) => handleValueChange("prescriptionAuthorizedRefills", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
{t("prescription.remainingRefills")}
|
{t("prescription.remainingRefills")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.prescriptionRemainingRefills}
|
value={form.prescriptionRemainingRefills}
|
||||||
onChange={(e) => handleValueChange("prescriptionRemainingRefills", e.target.value)}
|
onChange={(nextValue) => handleValueChange("prescriptionRemainingRefills", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
{t("prescription.lowThreshold")}
|
{t("prescription.lowThreshold")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={form.prescriptionLowRefillThreshold}
|
value={form.prescriptionLowRefillThreshold}
|
||||||
onChange={(e) => handleValueChange("prescriptionLowRefillThreshold", e.target.value)}
|
onChange={(nextValue) => handleValueChange("prescriptionLowRefillThreshold", nextValue)}
|
||||||
|
min={0}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="prescription-field">
|
<label className="prescription-field">
|
||||||
@@ -1362,22 +1372,24 @@ export function MedicationsPage() {
|
|||||||
<div className="blister-inputs">
|
<div className="blister-inputs">
|
||||||
<label>
|
<label>
|
||||||
{t("form.blisters.usage")}
|
{t("form.blisters.usage")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
pattern="[0-9]*\.?[0-9]*"
|
|
||||||
value={intake.usage}
|
value={intake.usage}
|
||||||
onChange={(e) => setIntakeValue(idx, "usage", e.target.value)}
|
onChange={(nextValue) => setIntakeValue(idx, "usage", nextValue)}
|
||||||
|
min={0.5}
|
||||||
|
step={0.5}
|
||||||
|
allowDecimal={true}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{t("form.blisters.everyDays")}
|
{t("form.blisters.everyDays")}
|
||||||
<input
|
<FormNumberStepper
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={intake.every}
|
value={intake.every}
|
||||||
onChange={(e) => setIntakeValue(idx, "every", e.target.value)}
|
onChange={(nextValue) => setIntakeValue(idx, "every", nextValue)}
|
||||||
|
min={1}
|
||||||
|
decrementLabel={decrementValueLabel}
|
||||||
|
incrementLabel={incrementValueLabel}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
+20
-5
@@ -1127,11 +1127,15 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
.blister-row .blister-inputs {
|
.blister-row .blister-inputs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.05fr) minmax(10.75rem, 1fr) minmax(7.25rem, 0.8fr);
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blister-row .blister-inputs > label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.blister-row .blister-inputs label.taken-by-field {
|
.blister-row .blister-inputs label.taken-by-field {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
@@ -1154,6 +1158,17 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Desktop edit sidebar can be narrow; avoid clipping right-side controls. */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.edit-sidebar .blister-row .blister-inputs {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-sidebar .blister-row .blister-inputs label.taken-by-field {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gap {
|
.gap {
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
@@ -3376,7 +3391,7 @@ button.has-validation-error {
|
|||||||
transition:
|
transition:
|
||||||
opacity 0.15s,
|
opacity 0.15s,
|
||||||
visibility 0.15s;
|
visibility 0.15s;
|
||||||
z-index: 100;
|
z-index: 1100;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3394,7 +3409,7 @@ button.has-validation-error {
|
|||||||
transition:
|
transition:
|
||||||
opacity 0.15s,
|
opacity 0.15s,
|
||||||
visibility 0.15s;
|
visibility 0.15s;
|
||||||
z-index: 101;
|
z-index: 1101;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tooltip aligned to left edge of icon (prevents clipping inside modals) */
|
/* Tooltip aligned to left edge of icon (prevents clipping inside modals) */
|
||||||
@@ -4335,7 +4350,7 @@ button.has-validation-error {
|
|||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.med-detail-modal .med-detail-body {
|
.med-detail-modal .med-detail-body {
|
||||||
@@ -4701,7 +4716,7 @@ button.has-validation-error {
|
|||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0 0 12px 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
|
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||||
|
|||||||
@@ -461,6 +461,7 @@
|
|||||||
|
|
||||||
.refill-number-stepper .stepper-btn.decrement {
|
.refill-number-stepper .stepper-btn.decrement {
|
||||||
order: initial;
|
order: initial;
|
||||||
|
background: color-mix(in srgb, var(--danger) 22%, var(--bg-tertiary));
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,13 +469,20 @@
|
|||||||
order: initial;
|
order: initial;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-left: 1px solid var(--border-primary);
|
border-left: 1px solid var(--border-primary);
|
||||||
background: color-mix(in srgb, var(--bg-tertiary) 85%, transparent);
|
background: color-mix(in srgb, var(--success) 22%, var(--bg-tertiary));
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.refill-number-stepper .stepper-btn:hover:not(:disabled) {
|
.refill-number-stepper .stepper-btn:hover:not(:disabled) {
|
||||||
filter: none;
|
filter: none;
|
||||||
background: color-mix(in srgb, var(--accent) 14%, var(--bg-tertiary));
|
}
|
||||||
|
|
||||||
|
.refill-number-stepper .stepper-btn.decrement:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--danger) 36%, var(--bg-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.refill-number-stepper .stepper-btn.increment:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--success) 36%, var(--bg-tertiary));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
@media (min-width: 641px) {
|
||||||
@@ -488,12 +496,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] .refill-number-stepper .stepper-btn.decrement {
|
[data-theme="light"] .refill-number-stepper .stepper-btn.decrement {
|
||||||
background: color-mix(in srgb, var(--bg-tertiary) 90%, transparent);
|
background: color-mix(in srgb, #dc2626 18%, white);
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] .refill-number-stepper .stepper-btn.increment {
|
[data-theme="light"] .refill-number-stepper .stepper-btn.increment {
|
||||||
background: color-mix(in srgb, var(--bg-tertiary) 90%, transparent);
|
background: color-mix(in srgb, #0f766e 18%, white);
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,6 +512,111 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Form stepper keeps symmetric - value + layout in all contexts (desktop/mobile). */
|
||||||
|
.form-number-stepper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper input {
|
||||||
|
order: 0;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper .stepper-btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-right: 1px solid var(--border-primary);
|
||||||
|
border-left: none;
|
||||||
|
background: color-mix(in srgb, var(--bg-tertiary) 85%, transparent);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper .stepper-btn.decrement {
|
||||||
|
order: -1;
|
||||||
|
background: color-mix(in srgb, var(--danger) 22%, var(--bg-tertiary));
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper .stepper-btn.increment {
|
||||||
|
order: 1;
|
||||||
|
border-right: none;
|
||||||
|
border-left: 1px solid var(--border-primary);
|
||||||
|
background: color-mix(in srgb, var(--success) 22%, var(--bg-tertiary));
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper .stepper-btn:hover:not(:disabled) {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper .stepper-btn.decrement:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--danger) 36%, var(--bg-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper .stepper-btn.increment:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--success) 36%, var(--bg-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight both controls when the center value field is focused (keyboard/click). */
|
||||||
|
.form-number-stepper:has(input:focus) .stepper-btn.decrement:not(:disabled),
|
||||||
|
.form-number-stepper:has(input:focus-visible) .stepper-btn.decrement:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--danger) 36%, var(--bg-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper:has(input:focus) .stepper-btn.increment:not(:disabled),
|
||||||
|
.form-number-stepper:has(input:focus-visible) .stepper-btn.increment:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--success) 36%, var(--bg-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dense schedule grids need a compact variant so the middle value stays visible. */
|
||||||
|
.blister-inputs .form-number-stepper,
|
||||||
|
.mobile-edit-form .blister-row .form-number-stepper {
|
||||||
|
grid-template-columns: 2.35rem minmax(2rem, 1fr) 2.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blister-inputs .form-number-stepper input,
|
||||||
|
.mobile-edit-form .blister-row .form-number-stepper input {
|
||||||
|
min-height: 2.35rem;
|
||||||
|
padding: 0.5rem 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blister-inputs .form-number-stepper .stepper-btn,
|
||||||
|
.mobile-edit-form .blister-row .form-number-stepper .stepper-btn {
|
||||||
|
min-width: 2.35rem;
|
||||||
|
min-height: 2.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.form-number-stepper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-number-stepper input {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .form-number-stepper .stepper-btn.decrement {
|
||||||
|
background: color-mix(in srgb, #dc2626 18%, white);
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .form-number-stepper .stepper-btn.increment {
|
||||||
|
background: color-mix(in srgb, #0f766e 18%, white);
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.form-number-stepper {
|
||||||
|
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.edit-stock-summary {
|
.edit-stock-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user