571d94bf7e
## Package Type Feature - Add 'blister' and 'bottle' package types for medications - Bottle type uses totalPills for capacity and looseTablets for current stock - Blister type continues to use packCount/blistersPerPack/pillsPerBlister - Add doseUnit field for flexible dosing (mg, ml, IU, etc.) - Full UI support in medication form and detail modal ## Per-Intake TakenBy - Move takenBy from medication level to individual intakes - Each intake schedule can now be assigned to a different person - Update scheduler-utils to handle per-intake takenBy - Update SharedSchedule to filter by per-intake takenBy - Backward compatible with existing medication data ## UI Improvements - Add PasswordInput component with show/hide toggle - Centralize stockThresholds in AppContext for consistent status display - Fix SharedSchedule sync issues with per-intake takenBy - Improve mobile editing experience ## Technical - Add migrations 0004 and 0005 for schema changes - Update all relevant tests (1064 tests passing) - Maintain backward compatibility with ALTER migrations
75 lines
1.7 KiB
TypeScript
75 lines
1.7 KiB
TypeScript
import { useState } from "react";
|
|
|
|
interface PasswordInputProps {
|
|
id: string;
|
|
value: string;
|
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
required?: boolean;
|
|
autoComplete?: string;
|
|
minLength?: number;
|
|
maxLength?: number;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export function PasswordInput({
|
|
id,
|
|
value,
|
|
onChange,
|
|
required,
|
|
autoComplete,
|
|
minLength,
|
|
maxLength,
|
|
placeholder,
|
|
}: PasswordInputProps) {
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
return (
|
|
<div className="password-input-wrapper">
|
|
<input
|
|
id={id}
|
|
type={showPassword ? "text" : "password"}
|
|
value={value}
|
|
onChange={onChange}
|
|
required={required}
|
|
autoComplete={autoComplete}
|
|
minLength={minLength}
|
|
maxLength={maxLength}
|
|
placeholder={placeholder}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="password-toggle-btn"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
tabIndex={-1}
|
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
>
|
|
{showPassword ? (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
|
<line x1="1" y1="1" x2="23" y2="23" />
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|