Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs`
2294ac57371ee75c597505bf4a3a4f5c7e1834ef # prettier, 304 files
6 changes: 3 additions & 3 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ name: Auto-merge

on:
workflow_run:
workflows: ['CI']
workflows: ["CI"]
types: [completed]
schedule:
- cron: '*/10 * * * *'
- cron: "*/10 * * * *"
workflow_dispatch: {}

# Declared on the CALLER as well as inside the reusable workflow: a called
Expand All @@ -46,4 +46,4 @@ jobs:
# SPACE-separated: the sweep word-splits this. A comma would become one
# bogus token, every dispatch would fail, and the only symptom would be
# that nothing deploys — while the sweep still reported success.
rearm_workflows: 'ci.yml deploy.yml'
rearm_workflows: "ci.yml deploy.yml"
21 changes: 21 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Build output and vendored trees — formatting these is noise.
node_modules
.next
dist
build
out
coverage
.turbo
.vercel
*.min.js
*.min.css

# Lockfiles are generated; prettier would rewrite them wholesale.
package-lock.json
pnpm-lock.yaml
yarn.lock

# Markdown is deliberately out of scope for now. Prettier rewraps prose, which
# is where it is most opinionated and least useful, and it would bury the real
# diff. Remove this line when you want docs formatted too.
*.md
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"arrowParens": "always",
"endOfLine": "lf"
}
83 changes: 63 additions & 20 deletions app/(admin)/admin.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
color: var(--white-60);
text-decoration: none;
font-size: 0.82rem;
transition: background var(--transition-fast), color var(--transition-fast);
transition:
background var(--transition-fast),
color var(--transition-fast);
display: flex;
align-items: center;
gap: 0.65rem;
Expand Down Expand Up @@ -142,7 +144,6 @@
background: var(--warn);
}


.mainWrap {
composes: mainWrap from "../shared.module.css";
}
Expand All @@ -163,7 +164,6 @@
composes: main from "../shared.module.css";
}


/* Shared admin UI — geometry from app/shared.module.css */
.pageTitle {
composes: pageTitle from "../shared.module.css";
Expand All @@ -181,9 +181,15 @@
composes: cardMb from "../shared.module.css";
}

.checkboxRow { composes: checkboxRow from "../shared.module.css"; }
.checkboxInput { composes: checkboxInput from "../shared.module.css"; }
.checkboxHint { composes: checkboxHint from "../shared.module.css"; }
.checkboxRow {
composes: checkboxRow from "../shared.module.css";
}
.checkboxInput {
composes: checkboxInput from "../shared.module.css";
}
.checkboxHint {
composes: checkboxHint from "../shared.module.css";
}

.table {
width: 100%;
Expand Down Expand Up @@ -310,12 +316,31 @@
margin-top: 0.2rem;
}

.phasePill[data-phase="intake"] { color: var(--muted); background: var(--muted-10); }
.phasePill[data-phase="assessment"] { color: var(--teal); background: var(--teal-10); }
.phasePill[data-phase="planning"] { color: var(--warn); background: var(--warn-10); }
.phasePill[data-phase="active"] { color: var(--teal); background: var(--teal-15); font-weight: 600; }
.phasePill[data-phase="review"] { color: var(--warn); background: var(--warn-10); }
.phasePill[data-phase="completed"] { color: var(--muted); background: var(--muted-8); }
.phasePill[data-phase="intake"] {
color: var(--muted);
background: var(--muted-10);
}
.phasePill[data-phase="assessment"] {
color: var(--teal);
background: var(--teal-10);
}
.phasePill[data-phase="planning"] {
color: var(--warn);
background: var(--warn-10);
}
.phasePill[data-phase="active"] {
color: var(--teal);
background: var(--teal-15);
font-weight: 600;
}
.phasePill[data-phase="review"] {
color: var(--warn);
background: var(--warn-10);
}
.phasePill[data-phase="completed"] {
color: var(--muted);
background: var(--muted-8);
}

/* ─── Sparkline ──────────────────────────────────────────────────────────────── */

Expand All @@ -333,9 +358,15 @@
flex-shrink: 0;
}

.sparkDot[data-level="low"] { background: var(--danger); }
.sparkDot[data-level="mid"] { background: var(--warn); }
.sparkDot[data-level="high"] { background: var(--teal); }
.sparkDot[data-level="low"] {
background: var(--danger);
}
.sparkDot[data-level="mid"] {
background: var(--warn);
}
.sparkDot[data-level="high"] {
background: var(--teal);
}

/* ─── Card label (used in section headers within detail cards) ─────────────── */

Expand Down Expand Up @@ -903,7 +934,7 @@
}

.stretchedLink::after {
content: '';
content: "";
position: absolute;
inset: 0;
z-index: 0;
Expand Down Expand Up @@ -1010,7 +1041,10 @@
cursor: pointer;
text-transform: capitalize;
letter-spacing: 0.04em;
transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
transition:
background var(--transition-fast),
color var(--transition-fast),
border-color var(--transition-fast);
}

.filterTab:hover,
Expand Down Expand Up @@ -1903,9 +1937,18 @@

/* Tone is the whole design of this screen: a section that shouts must be one
the reader can trust, so only two of the five are allowed to. */
.inboxTitleUrgent { composes: inboxTitle; color: var(--danger); }
.inboxTitleAttention { composes: inboxTitle; color: var(--warn); }
.inboxTitleRoutine { composes: inboxTitle; color: var(--muted); }
.inboxTitleUrgent {
composes: inboxTitle;
color: var(--danger);
}
.inboxTitleAttention {
composes: inboxTitle;
color: var(--warn);
}
.inboxTitleRoutine {
composes: inboxTitle;
color: var(--muted);
}

.inboxCount {
font-size: 0.7rem;
Expand Down
70 changes: 47 additions & 23 deletions app/(admin)/admin/bookings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import styles from "../../admin.module.css";
import {
BOOKING_STATUS, BOOKING_STATUS_CONFIG, BOOKING_STATUS_VALUES,
BOOKING_TYPE_CONFIG, MACHINE_TYPE_CONFIG,
type BookingRowWithUser, type BookingStatus,
BOOKING_STATUS,
BOOKING_STATUS_CONFIG,
BOOKING_STATUS_VALUES,
BOOKING_TYPE_CONFIG,
MACHINE_TYPE_CONFIG,
type BookingRowWithUser,
type BookingStatus,
} from "@/lib/config/booking-status";
import { BookingActions } from "@/components/clinical/BookingActions";
import { formatDateShort, formatDateNumeric, formatSlotDay, formatSlotTime } from "@/lib/utils/format";
import {
formatDateShort,
formatDateNumeric,
formatSlotDay,
formatSlotTime,
} from "@/lib/utils/format";
import { ADMIN_ROUTES } from "@/lib/config/routes";
import { LoadingState } from "@/components/LoadingState";


// "upcoming" answers the clinician's daily question — who am I seeing next? —
// and is the default: slot bookings are born confirmed, so the old "pending"
// default showed an empty list on a normal day.
Expand All @@ -33,7 +41,10 @@ export default function AdminBookingsPage() {
const load = useCallback(async () => {
try {
const res = await fetch("/api/bookings");
if (!res.ok) { setLoadError(true); return; }
if (!res.ok) {
setLoadError(true);
return;
}
const data = await res.json();
setBookings(data.data ?? []);
} catch {
Expand All @@ -43,7 +54,9 @@ export default function AdminBookingsPage() {
}
}, []);

useEffect(() => { load(); }, [load]);
useEffect(() => {
load();
}, [load]);

// Clears the admin nav's "new bookings" badge — fire-and-forget, the same
// "I looked at this page" signal the patient portal's goals page sends.
Expand Down Expand Up @@ -72,7 +85,12 @@ export default function AdminBookingsPage() {

const now = Date.now();
const upcoming = bookings
.filter((b) => b.scheduledAt && new Date(b.scheduledAt).getTime() >= now && ACTIVE_STATUSES.includes(b.status))
.filter(
(b) =>
b.scheduledAt &&
new Date(b.scheduledAt).getTime() >= now &&
ACTIVE_STATUSES.includes(b.status),
)
.sort((a, b) => new Date(a.scheduledAt!).getTime() - new Date(b.scheduledAt!).getTime());
const filtered =
filter === "upcoming"
Expand Down Expand Up @@ -100,7 +118,9 @@ export default function AdminBookingsPage() {
went unused: nobody had connected a real calendar. */}
<p className={styles.pageSub}>
Working hours, intake and calendar sync now live on{" "}
<Link href={ADMIN_ROUTES.profile} className={styles.cellLink}>My Profile →</Link>
<Link href={ADMIN_ROUTES.profile} className={styles.cellLink}>
My Profile →
</Link>
</p>

<div className={styles.filterBar}>
Expand All @@ -111,7 +131,8 @@ export default function AdminBookingsPage() {
onClick={() => setFilter(f)}
className={`${styles.filterTab}${filter === f ? ` ${styles.filterTabActive}` : ""}`}
>
{f}{f === BOOKING_STATUS.pending && pendingCount > 0 ? ` (${pendingCount})` : ""}
{f}
{f === BOOKING_STATUS.pending && pendingCount > 0 ? ` (${pendingCount})` : ""}
</button>
))}
</div>
Expand Down Expand Up @@ -155,22 +176,29 @@ export default function AdminBookingsPage() {
{b.user.name ?? <span className={styles.cellMuted}>No name</span>}
</div>
<div className={styles.cellSub}>{b.user.email}</div>
<Link href={`${ADMIN_ROUTES.patients}/${b.user.id}`} className={styles.cellLink}>
<Link
href={`${ADMIN_ROUTES.patients}/${b.user.id}`}
className={styles.cellLink}
>
View patient →
</Link>
</td>
<td className={styles.cellNowrap}>
<div>{BOOKING_TYPE_CONFIG[b.bookingType]?.label ?? b.bookingType}</div>
{b.machineType && (
<div className={styles.cellSub}>{MACHINE_TYPE_CONFIG[b.machineType]?.label}</div>
<div className={styles.cellSub}>
{MACHINE_TYPE_CONFIG[b.machineType]?.label}
</div>
)}
</td>
<td className={styles.cellNowrap}>
{b.scheduledAt
? `${formatSlotDay(b.scheduledAt)}, ${formatSlotTime(b.scheduledAt)}`
: b.preferredDate
? formatDateShort(b.preferredDate)
: <span className={styles.cellMuted}>—</span>}
{b.scheduledAt ? (
`${formatSlotDay(b.scheduledAt)}, ${formatSlotTime(b.scheduledAt)}`
) : b.preferredDate ? (
formatDateShort(b.preferredDate)
) : (
<span className={styles.cellMuted}>—</span>
)}
</td>
<td className={styles.tdNotes}>
{b.notes ? (
Expand All @@ -179,13 +207,9 @@ export default function AdminBookingsPage() {
<span className={styles.cellMuted}>—</span>
)}
</td>
<td className={styles.cellNowrap}>
{formatDateNumeric(b.createdAt)}
</td>
<td className={styles.cellNowrap}>{formatDateNumeric(b.createdAt)}</td>
<td>
<span className={`${styles.badge} ${s.badgeClass}`}>
{s.label}
</span>
<span className={`${styles.badge} ${s.badgeClass}`}>{s.label}</span>
{/* A late cancellation is clinical information, not just
accounting — a pattern of them is worth a conversation. */}
{b.lateCancellation && (
Expand Down
Loading
Loading