Clinic — In-clinic Dispense Counter
A single-patient pharmacy dispensing screen that pairs a selected Rx detail card — patient, drug, dose, sig and prescriber — with a dispense form for quantity, lot and expiry plus a barcode-scan field that simulates a scanner and auto-fills a verified lot. A live pharmacy label preview updates as you type, a counseling checklist gates release, and Dispense and print label validates everything before showing a calm dispensed done-state with toast feedback.
MCP
程式碼
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
button {
font: inherit;
}
input,
button {
font-family: inherit;
}
:focus-visible {
outline: 2px solid var(--teal);
outline-offset: 2px;
}
/* ── Layout ── */
.dispense {
max-width: 920px;
margin: 0 auto;
padding: 32px 20px 64px;
display: flex;
flex-direction: column;
gap: 20px;
}
.head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.eyebrow {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--teal-d);
}
.head h1 {
font-size: 1.7rem;
font-weight: 800;
letter-spacing: -0.02em;
margin-top: 4px;
}
.head-meta {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.pill {
font-size: 0.76rem;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px 12px;
}
/* ── Rx detail ── */
.rx {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--shadow-1);
display: flex;
flex-direction: column;
gap: 18px;
}
.rx-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.rx-id {
display: flex;
align-items: baseline;
gap: 8px;
}
.rx-tag {
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.rx-num {
font-size: 1.2rem;
font-weight: 800;
letter-spacing: -0.01em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.rx-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.rx-block .label {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.rx-block .value {
font-size: 1.02rem;
font-weight: 700;
margin-top: 3px;
}
.rx-block .sub {
font-size: 0.82rem;
color: var(--muted);
margin-top: 2px;
}
.drug {
border: 1px solid var(--line);
border-radius: var(--r-md);
background: linear-gradient(160deg, var(--teal-50), #f4faf9);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 14px;
}
.drug-name {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
}
.drug-strength {
font-size: 1.12rem;
font-weight: 800;
color: var(--teal-700);
letter-spacing: -0.01em;
}
.drug-form {
font-size: 0.85rem;
color: var(--ink-2);
}
.drug-facts {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.drug-facts div {
display: flex;
flex-direction: column;
gap: 2px;
}
.drug-facts dt {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.drug-facts dd {
font-size: 0.92rem;
font-weight: 600;
color: var(--ink);
}
.sig {
font-size: 0.9rem;
color: var(--ink-2);
line-height: 1.55;
background: var(--white);
border: 1px dashed var(--line-2);
border-radius: var(--r-sm);
padding: 10px 12px;
}
.sig-tag {
display: inline-block;
font-size: 0.68rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--teal-d);
background: var(--teal-50);
border-radius: 6px;
padding: 2px 7px;
margin-right: 6px;
vertical-align: 1px;
}
/* ── Two columns ── */
.cols {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 20px;
align-items: start;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--shadow-1);
}
.card-title {
font-size: 1.05rem;
font-weight: 700;
letter-spacing: -0.01em;
}
/* ── Form ── */
.form {
display: flex;
flex-direction: column;
gap: 18px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.field label,
.checklist legend {
font-size: 0.82rem;
font-weight: 700;
color: var(--ink-2);
}
.field input[type="text"],
.field input[type="number"],
.field input[type="date"] {
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 11px 13px;
font-size: 0.95rem;
color: var(--ink);
background: var(--white);
width: 100%;
transition: border-color 0.15s, box-shadow 0.15s;
}
.field input:focus {
outline: none;
border-color: var(--teal);
box-shadow: 0 0 0 3px rgba(18, 156, 147, 0.16);
}
.field input.invalid {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(212, 80, 62, 0.14);
}
.hint {
font-size: 0.8rem;
color: var(--muted);
}
.hint.warn {
color: var(--warn);
font-weight: 600;
}
/* ── Quantity stepper ── */
.qty {
display: grid;
grid-template-columns: 44px 1fr 44px;
gap: 0;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
overflow: hidden;
}
.qty input {
border: none !important;
border-radius: 0 !important;
text-align: center;
font-weight: 700;
font-variant-numeric: tabular-nums;
box-shadow: none !important;
}
.qty input:focus {
background: var(--teal-50);
}
.step {
border: none;
background: var(--teal-50);
color: var(--teal-d);
font-size: 1.25rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
line-height: 1;
}
.step:hover {
background: #d6ede9;
}
.step:active {
background: #c8e6e1;
}
/* ── Scan field ── */
.scan {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 4px 12px 4px 14px;
background: var(--white);
transition: border-color 0.15s, box-shadow 0.15s, background 0.2s;
}
.scan:focus-within {
border-color: var(--teal);
box-shadow: 0 0 0 3px rgba(18, 156, 147, 0.16);
}
.scan[data-state="ok"] {
border-color: var(--ok);
background: rgba(47, 158, 111, 0.06);
}
.scan[data-state="bad"] {
border-color: var(--danger);
background: rgba(212, 80, 62, 0.05);
}
.scan-icon {
font-size: 1.1rem;
color: var(--muted);
letter-spacing: -2px;
}
.scan input {
border: none !important;
box-shadow: none !important;
padding: 9px 0 !important;
background: transparent;
font-size: 0.95rem;
width: 100%;
}
.scan input:focus {
outline: none;
}
.scan-status {
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
}
.scan[data-state="ok"] .scan-status {
color: var(--ok);
}
.scan[data-state="bad"] .scan-status {
color: var(--danger);
}
.chip {
border: 1px solid var(--line-2);
background: var(--white);
border-radius: 999px;
padding: 2px 9px;
font-size: 0.76rem;
font-weight: 600;
color: var(--teal-d);
cursor: pointer;
font-variant-numeric: tabular-nums;
transition: background 0.15s, border-color 0.15s;
}
.chip:hover {
background: var(--teal-50);
border-color: var(--teal);
}
/* ── Checklist ── */
.checklist {
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px 12px;
display: flex;
flex-direction: column;
gap: 10px;
}
.checklist legend {
padding: 0 6px;
}
.check {
display: grid;
grid-template-columns: auto 1fr;
gap: 11px;
align-items: start;
cursor: pointer;
padding: 8px 8px;
border-radius: var(--r-sm);
transition: background 0.15s;
}
.check:hover {
background: var(--teal-50);
}
.check input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.box {
width: 20px;
height: 20px;
border: 2px solid var(--line-2);
border-radius: 6px;
margin-top: 1px;
display: grid;
place-items: center;
transition: background 0.15s, border-color 0.15s;
}
.box::after {
content: "✓";
color: #fff;
font-size: 0.78rem;
font-weight: 800;
opacity: 0;
transform: scale(0.6);
transition: opacity 0.12s, transform 0.12s;
}
.check input:checked ~ .box {
background: var(--ok);
border-color: var(--ok);
}
.check input:checked ~ .box::after {
opacity: 1;
transform: scale(1);
}
.check input:focus-visible ~ .box {
outline: 2px solid var(--teal);
outline-offset: 2px;
}
.check-text {
display: flex;
flex-direction: column;
gap: 1px;
}
.check-text strong {
font-size: 0.9rem;
font-weight: 600;
}
.check-text small {
font-size: 0.78rem;
color: var(--muted);
}
.checklist-meter {
font-size: 0.8rem;
font-weight: 600;
color: var(--ink-2);
padding: 2px 8px 0;
}
.checklist-meter span {
color: var(--teal-d);
}
/* ── Buttons ── */
.btn {
border: none;
border-radius: 11px;
padding: 12px 18px;
font-weight: 700;
font-size: 0.92rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, border-color 0.15s, opacity 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--teal-d);
color: #fff;
box-shadow: 0 6px 16px rgba(12, 122, 115, 0.22);
}
.btn.primary:hover {
background: var(--teal-700);
}
.btn.ghost {
background: var(--white);
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.btn.ghost:hover {
background: var(--teal-50);
border-color: var(--teal);
color: var(--teal-d);
}
/* ── Badge ── */
.badge {
font-size: 0.74rem;
font-weight: 700;
padding: 5px 11px;
border-radius: 999px;
white-space: nowrap;
}
.badge.ok {
background: rgba(47, 158, 111, 0.14);
color: var(--ok);
}
.badge.done {
background: var(--teal-50);
color: var(--teal-d);
}
/* ── Label preview ── */
.preview {
position: sticky;
top: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.preview-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.live-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5);
}
70% {
box-shadow: 0 0 0 7px rgba(47, 158, 111, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0);
}
}
.label {
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: #fffef9;
padding: 16px 18px;
font-family: "Inter", monospace;
box-shadow: inset 0 0 0 4px #fff, 0 2px 8px rgba(16, 50, 47, 0.06);
}
.label hr {
border: none;
border-top: 1px dashed var(--line-2);
margin: 10px 0;
}
.label-pharm {
display: flex;
flex-direction: column;
gap: 1px;
}
.label-pharm strong {
font-size: 0.95rem;
font-weight: 800;
color: var(--ink);
}
.label-pharm span {
font-size: 0.74rem;
color: var(--muted);
}
.label-rx {
font-size: 0.78rem;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.label-patient {
font-size: 1.05rem;
font-weight: 800;
margin-top: 2px;
}
.label-drug {
font-size: 0.9rem;
font-weight: 600;
color: var(--teal-700);
margin-top: 2px;
}
.label-qty {
font-size: 0.82rem;
color: var(--ink-2);
margin-top: 4px;
font-variant-numeric: tabular-nums;
}
.label-sig {
font-size: 0.86rem;
color: var(--ink);
margin-top: 6px;
line-height: 1.5;
}
.label-foot {
font-size: 0.72rem;
color: var(--muted);
line-height: 1.5;
}
.preview-note {
font-size: 0.78rem;
color: var(--muted);
text-align: center;
}
/* ── Done state ── */
.done {
display: flex;
align-items: center;
gap: 16px;
background: linear-gradient(150deg, var(--teal-50), #eafaf6);
border: 1px solid rgba(47, 158, 111, 0.3);
border-radius: var(--r-lg);
padding: 20px 22px;
box-shadow: var(--shadow-1);
animation: rise 0.3s ease;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.done-icon {
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--ok);
color: #fff;
display: grid;
place-items: center;
font-size: 1.4rem;
font-weight: 800;
flex-shrink: 0;
}
.done-text {
flex: 1;
}
.done-text h2 {
font-size: 1.15rem;
font-weight: 800;
color: var(--teal-700);
}
.done-text p {
font-size: 0.88rem;
color: var(--ink-2);
margin-top: 1px;
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
}
@media (max-width: 820px) {
.cols {
grid-template-columns: 1fr;
}
.preview {
position: static;
}
}
@media (max-width: 520px) {
.dispense {
padding: 24px 14px 48px;
}
.head h1 {
font-size: 1.45rem;
}
.rx,
.card {
padding: 18px;
}
.rx-grid {
grid-template-columns: 1fr;
}
.drug-facts {
grid-template-columns: 1fr 1fr;
}
.field-row {
grid-template-columns: 1fr;
}
.done {
flex-direction: column;
align-items: flex-start;
}
}
/* Visibility guard: honor the [hidden] attribute over base display */
.done[hidden] {
display: none;
}// ── In-clinic Dispense Counter ──────────────────────────────────────────────
// Vanilla JS only. Live label preview, simulated barcode scan + lot validation,
// counseling checklist gate, and a dispensed done-state with toast feedback.
(function () {
"use strict";
// ── Elements ──
const form = document.getElementById("dispense-form");
const qty = document.getElementById("qty");
const qtyUp = document.getElementById("qty-up");
const qtyDown = document.getElementById("qty-down");
const scanWrap = document.querySelector(".scan");
const scan = document.getElementById("scan");
const scanStatus = document.getElementById("scan-status");
const lot = document.getElementById("lot");
const expiry = document.getElementById("expiry");
const expiryWarn = document.getElementById("expiry-warn");
const counselBoxes = Array.from(
document.querySelectorAll('input[name="counsel"]')
);
const counselCount = document.getElementById("counsel-count");
const dispenseBtn = document.getElementById("dispense-btn");
const done = document.getElementById("done");
const doneDetail = document.getElementById("done-detail");
const nextBtn = document.getElementById("next-btn");
// Label fields
const lbl = {
date: document.getElementById("label-date"),
qty: document.getElementById("label-qty"),
lot: document.getElementById("label-lot"),
exp: document.getElementById("label-exp"),
};
// ── Known stock barcodes → lot data (simulated formulary) ──
const STOCK = {
"NDC-0093-4155-78": { lot: "A23F091", exp: "2027-04-30" },
"NDC-0093-4155-79": { lot: "A23F114", exp: "2027-06-30" },
};
const QTY_MIN = 1;
const QTY_MAX = 120;
// Lot is considered "verified" only when set by a valid scan.
let lotVerified = false;
// ── Toast helper ──
let toastTimer = null;
const toastEl = document.getElementById("toast");
function toast(msg) {
toastEl.textContent = msg;
toastEl.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastEl.hidden = true;
}, 2600);
}
// ── Date formatting ──
const MONTHS = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
function fmtDate(iso) {
if (!iso) return "—";
const [y, m, d] = iso.split("-").map(Number);
if (!y || !m || !d) return "—";
return `${String(d).padStart(2, "0")} ${MONTHS[m - 1]} ${y}`;
}
function todayISO() {
const t = new Date();
return [
t.getFullYear(),
String(t.getMonth() + 1).padStart(2, "0"),
String(t.getDate()).padStart(2, "0"),
].join("-");
}
// ── Live label sync ──
function renderLabel() {
lbl.qty.textContent = qty.value || "0";
lbl.lot.textContent = lot.value.trim() || "——";
lbl.exp.textContent = fmtDate(expiry.value);
lbl.date.textContent = fmtDate(todayISO());
}
// ── Quantity stepper ──
function clampQty() {
let v = parseInt(qty.value, 10);
if (isNaN(v)) v = QTY_MIN;
v = Math.min(QTY_MAX, Math.max(QTY_MIN, v));
qty.value = v;
return v;
}
function bumpQty(delta) {
qty.value = clampQty() + delta;
clampQty();
renderLabel();
}
qtyUp.addEventListener("click", () => bumpQty(1));
qtyDown.addEventListener("click", () => bumpQty(-1));
qty.addEventListener("input", renderLabel);
qty.addEventListener("blur", () => {
clampQty();
renderLabel();
});
// ── Expiry validation (warn if expired or near) ──
function checkExpiry() {
expiry.classList.remove("invalid");
if (!expiry.value) {
expiryWarn.hidden = true;
return;
}
const exp = new Date(expiry.value + "T00:00:00");
const now = new Date(todayISO() + "T00:00:00");
const days = Math.round((exp - now) / 86400000);
if (days < 0) {
expiryWarn.hidden = false;
expiryWarn.textContent = "Lot expired — do not dispense.";
expiryWarn.classList.add("warn");
expiry.classList.add("invalid");
} else if (days <= 90) {
expiryWarn.hidden = false;
expiryWarn.textContent = `Short-dated: expires in ${days} day${days === 1 ? "" : "s"}.`;
expiryWarn.classList.add("warn");
} else {
expiryWarn.hidden = true;
}
renderLabel();
}
expiry.addEventListener("input", checkExpiry);
// ── Lot field (manual edits invalidate scan verification) ──
lot.addEventListener("input", () => {
lotVerified = false;
setScanState("idle", "");
renderLabel();
});
// ── Barcode scan simulation ──
function setScanState(state, msg) {
scanWrap.dataset.state = state;
scanStatus.textContent = msg;
}
function processScan(raw) {
const code = (raw || "").trim().toUpperCase();
if (!code) {
setScanState("idle", "");
return;
}
const match = STOCK[code];
if (match) {
lot.value = match.lot;
expiry.value = match.exp;
lotVerified = true;
setScanState("ok", "Lot verified");
checkExpiry();
renderLabel();
toast(`Scan matched · lot ${match.lot}`);
} else {
lotVerified = false;
setScanState("bad", "Not in stock");
toast("Barcode not recognized — check the bottle.");
}
}
// Enter simulates the scanner's terminating keystroke.
scan.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
processScan(scan.value);
}
});
// Hardware scanners type fast then stop; debounce as a fallback.
let scanTimer = null;
scan.addEventListener("input", () => {
setScanState("idle", "Scanning…");
clearTimeout(scanTimer);
scanTimer = setTimeout(() => {
if (scan.value.trim()) processScan(scan.value);
else setScanState("idle", "");
}, 550);
});
// Sample-code chip(s)
document.querySelectorAll(".chip[data-fill]").forEach((chip) => {
chip.addEventListener("click", () => {
scan.value = chip.dataset.fill;
processScan(scan.value);
scan.focus();
});
});
// ── Counseling checklist ──
function counselDone() {
return counselBoxes.filter((b) => b.checked).length;
}
function syncCounsel() {
counselCount.textContent = String(counselDone());
}
counselBoxes.forEach((b) => b.addEventListener("change", syncCounsel));
// ── Submit / validate ──
function flash(el) {
el.classList.add("invalid");
el.focus();
setTimeout(() => el.classList.remove("invalid"), 1600);
}
form.addEventListener("submit", (e) => {
e.preventDefault();
const q = clampQty();
if (!q) {
flash(qty);
toast("Enter a quantity to dispense.");
return;
}
if (!lot.value.trim()) {
flash(lot);
toast("Scan or enter a lot number first.");
scan.focus();
return;
}
if (!lotVerified) {
toast("Lot not verified — scan the stock bottle to confirm.");
flash(lot);
scan.focus();
return;
}
if (expiry.classList.contains("invalid")) {
toast("Expiry issue — resolve before dispensing.");
expiry.focus();
return;
}
if (counselDone() < counselBoxes.length) {
const firstUnchecked = counselBoxes.find((b) => !b.checked);
if (firstUnchecked) firstUnchecked.focus();
toast("Complete all counseling points before dispensing.");
return;
}
// Success → lock the form, reveal done-state.
renderLabel();
form
.querySelectorAll("input, button")
.forEach((el) => (el.disabled = true));
setScanState("ok", "Lot verified");
const rxStatus = document.getElementById("rx-status");
if (rxStatus) {
rxStatus.textContent = "Dispensed";
rxStatus.classList.remove("ok");
rxStatus.classList.add("done");
}
doneDetail.textContent = `Label printed · ${q} dispensed · lot ${lot.value.trim()}.`;
done.hidden = false;
done.scrollIntoView({ behavior: "smooth", block: "nearest" });
toast("Dispensed — label sent to printer.");
});
// ── Reset for next prescription ──
nextBtn.addEventListener("click", () => {
form
.querySelectorAll("input, button")
.forEach((el) => (el.disabled = false));
form.reset();
lotVerified = false;
setScanState("idle", "");
expiryWarn.hidden = true;
qty.value = 30;
expiry.value = "2027-04-30";
const rxStatus = document.getElementById("rx-status");
if (rxStatus) {
rxStatus.textContent = "Ready to dispense";
rxStatus.classList.remove("done");
rxStatus.classList.add("ok");
}
done.hidden = true;
syncCounsel();
renderLabel();
scan.focus();
toast("Counter cleared — next prescription ready.");
});
// ── Init ──
syncCounsel();
checkExpiry();
renderLabel();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Dispense Counter · Northpoint Clinic Pharmacy</title>
</head>
<body>
<main class="dispense" aria-labelledby="page-title">
<header class="head">
<div class="head-titles">
<p class="eyebrow">Northpoint Clinic · In-clinic Pharmacy</p>
<h1 id="page-title">Dispense counter</h1>
</div>
<div class="head-meta">
<span class="pill">Station 2</span>
<span class="pill">Pharmacist: A. Reyes, RPh</span>
</div>
</header>
<!-- ── Selected Rx detail ── -->
<section class="rx" aria-label="Selected prescription">
<div class="rx-top">
<div class="rx-id">
<span class="rx-tag">Rx #</span>
<span class="rx-num">RX-4471082</span>
</div>
<span class="badge ok" id="rx-status">Ready to dispense</span>
</div>
<div class="rx-grid">
<div class="rx-block rx-patient">
<p class="label">Patient</p>
<p class="value">Marcus Adeyemi</p>
<p class="sub">DOB 14 Mar 1979 · MRN 88213 · Allergy: Penicillin</p>
</div>
<div class="rx-block">
<p class="label">Prescriber</p>
<p class="value">Dr. Lena Okafor</p>
<p class="sub">Internal Medicine · NPI 1093847562</p>
</div>
</div>
<div class="drug">
<div class="drug-name">
<span class="drug-strength">Amoxicillin 500 mg</span>
<span class="drug-form">capsule, oral</span>
</div>
<dl class="drug-facts">
<div>
<dt>Dose</dt>
<dd>1 capsule</dd>
</div>
<div>
<dt>Frequency</dt>
<dd>three times daily</dd>
</div>
<div>
<dt>Days supply</dt>
<dd>10 days</dd>
</div>
<div>
<dt>Refills</dt>
<dd>0 remaining</dd>
</div>
</dl>
<p class="sig">
<span class="sig-tag">Sig</span>
Take 1 capsule by mouth three times daily for 10 days. Take with food.
Finish the full course.
</p>
</div>
</section>
<div class="cols">
<!-- ── Dispense form ── -->
<form class="card form" id="dispense-form" novalidate>
<h2 class="card-title">Dispense details</h2>
<div class="field">
<label for="qty">Quantity dispensed</label>
<div class="qty">
<button type="button" class="step" id="qty-down" aria-label="Decrease quantity">−</button>
<input
id="qty"
name="qty"
type="number"
inputmode="numeric"
min="1"
max="120"
value="30"
aria-describedby="qty-hint"
/>
<button type="button" class="step" id="qty-up" aria-label="Increase quantity">+</button>
</div>
<p class="hint" id="qty-hint">Prescribed: 30 capsules (1 cap × 3/day × 10 days)</p>
</div>
<div class="field">
<label for="scan">Scan stock bottle</label>
<div class="scan" data-state="idle">
<span class="scan-icon" aria-hidden="true">▦</span>
<input
id="scan"
name="scan"
type="text"
placeholder="Focus here and scan barcode…"
autocomplete="off"
aria-describedby="scan-hint"
/>
<span class="scan-status" id="scan-status" aria-live="polite"></span>
</div>
<p class="hint" id="scan-hint">
Type a code and press Enter to simulate a scan. Try
<button type="button" class="chip" data-fill="NDC-0093-4155-78">NDC-0093-4155-78</button>.
</p>
</div>
<div class="field-row">
<div class="field">
<label for="lot">Lot #</label>
<input id="lot" name="lot" type="text" placeholder="e.g. A23F091" autocomplete="off" />
</div>
<div class="field">
<label for="expiry">Expiry date</label>
<input id="expiry" name="expiry" type="date" value="2027-04-30" />
</div>
</div>
<p class="hint" id="expiry-warn" hidden></p>
<fieldset class="checklist">
<legend>Counseling checklist</legend>
<label class="check">
<input type="checkbox" name="counsel" value="food" />
<span class="box" aria-hidden="true"></span>
<span class="check-text">
<strong>Take with food</strong>
<small>Reduces stomach upset; space doses evenly.</small>
</span>
</label>
<label class="check">
<input type="checkbox" name="counsel" value="effects" />
<span class="box" aria-hidden="true"></span>
<span class="check-text">
<strong>Side effects reviewed</strong>
<small>Nausea, rash — stop and call if breathing difficulty.</small>
</span>
</label>
<label class="check">
<input type="checkbox" name="counsel" value="questions" />
<span class="box" aria-hidden="true"></span>
<span class="check-text">
<strong>Questions answered</strong>
<small>Patient confirmed understanding of the full course.</small>
</span>
</label>
<p class="checklist-meter">
<span id="counsel-count">0</span> of 3 confirmed
</p>
</fieldset>
<button type="submit" class="btn primary" id="dispense-btn">
Dispense & print label
</button>
</form>
<!-- ── Live label preview ── -->
<aside class="card preview" aria-label="Pharmacy label preview">
<div class="preview-head">
<h2 class="card-title">Label preview</h2>
<span class="live-dot" aria-hidden="true"></span>
</div>
<div class="label" id="label">
<div class="label-pharm">
<strong>Northpoint Clinic Pharmacy</strong>
<span>114 Harbor Way · (555) 018-2240</span>
</div>
<hr />
<p class="label-rx">Rx <b>RX-4471082</b> · <span id="label-date">—</span></p>
<p class="label-patient" id="label-patient">Marcus Adeyemi</p>
<p class="label-drug" id="label-drug">Amoxicillin 500 mg capsule</p>
<p class="label-qty">Qty: <b id="label-qty">30</b> · Lot <b id="label-lot">——</b></p>
<p class="label-sig" id="label-sig">
Take 1 capsule by mouth three times daily for 10 days. Take with food.
</p>
<hr />
<p class="label-foot">
Prescriber: Dr. Lena Okafor · Exp <span id="label-exp">—</span><br />
Keep out of reach of children.
</p>
</div>
<p class="preview-note">Updates live as you complete the form.</p>
</aside>
</div>
<!-- ── Done state ── -->
<section class="done" id="done" hidden aria-live="polite">
<div class="done-icon" aria-hidden="true">✓</div>
<div class="done-text">
<h2>Dispensed</h2>
<p id="done-detail">Label printed for Marcus Adeyemi.</p>
</div>
<button type="button" class="btn ghost" id="next-btn">Next prescription</button>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>In-clinic Dispense Counter
A focused counter screen for filling a single prescription at the clinic pharmacy. The selected Rx leads the page — patient, prescriber, the drug strength and form, dose, days supply, refills and the full sig — so the dispenser can confirm the right fill before touching stock. Below it, the dispense form collects the quantity with a stepper, the lot number, an expiry date and a barcode-scan field. Typing a code and pressing Enter simulates a hardware scan: a recognised stock barcode auto-fills the lot and expiry and marks the lot verified, while an unknown code is flagged in red.
A live pharmacy label preview sits alongside the form and updates as you work, mirroring the patient, drug, sig, quantity, lot, fill date and expiry exactly as they would print. Editing the lot by hand clears its verified state, and short-dated or expired lots raise an inline warning so they can be caught early.
Before anything is released, a three-point counseling checklist — take with food, side effects reviewed, questions answered — must be confirmed. Dispense and print label validates the quantity, a verified lot, the expiry and the full checklist, then locks the form and reveals a calm Dispensed done-state with a toast. Next prescription clears the counter for the following patient. Everything runs on vanilla JS with no dependencies.
Illustrative UI only — not intended for real medical use.