Move all three skills into skills/; add root README; fix paths
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
312742cfef
commit
d95e5d452d
84 files changed
+97
-31
No files matched your search
@@ -0,0 +1,301 @@
|
||||
/* EXAMPLE ONLY: an extract from the Superbill Review prototype, kept as reference for porting. ECW/eClinicalWorks, Tebra, 'Example Client', SPB_* tables and patient/ticket wording are that project's examples. Don't copy them into a new project. */
|
||||
import { useState } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BatchStage, BoardTab } from '../types'
|
||||
import type { Project } from '../project'
|
||||
import { PROJECT_LABEL } from '../project'
|
||||
import { IconSpend, IconPage, IconLoop, IconIdCard, IconPatients } from './icons'
|
||||
|
||||
/**
|
||||
* The app-wide section rail, at a fixed 236px.
|
||||
*
|
||||
* It used to collapse to a 56px icon strip and expand to an overlay on hover.
|
||||
* That is gone with the reskin: the rail is now always labelled, which is what
|
||||
* the approved mockup shows and what removes the "hover to find out what these
|
||||
* icons are" step from every navigation.
|
||||
*
|
||||
* Two projects, one rail: ECW's boards (templates 1-6) and Tebra's (template 7
|
||||
* / Example Client), each under its own group header. See project.ts for how a
|
||||
* batch or run is assigned to one or the other.
|
||||
*
|
||||
* Selection and drag-and-drop-to-move-stage behaviour are unchanged — only the
|
||||
* shape of the control moved.
|
||||
*/
|
||||
export function Sidebar({
|
||||
tab,
|
||||
onTab,
|
||||
counts,
|
||||
onDropBatch,
|
||||
onDropRun,
|
||||
isAdmin,
|
||||
projects,
|
||||
}: {
|
||||
tab: BoardTab
|
||||
onTab: (t: BoardTab) => void
|
||||
/** Per project, per stage — each section shows its own queue depth. */
|
||||
counts: Record<Project, Record<BatchStage, number>>
|
||||
/** A batch card dragged onto a section moves to that stage. */
|
||||
onDropBatch: (batchId: string, stage: BatchStage) => void
|
||||
/** A single document dragged onto Automation is gathered into its own batch. */
|
||||
onDropRun?: (jobId: string) => void
|
||||
/** Gates each section's ROI item, and the single account-level People
|
||||
* item below them all — both are admin-and-above. */
|
||||
isAdmin: boolean
|
||||
/** Which sections render at all. A scoped admin sees one; everyone else sees
|
||||
* both. Independent of `isAdmin`: an operator sees both sections' boards
|
||||
* with ROI/People hidden inside each, while a scoped admin has a whole
|
||||
* section absent regardless of role. */
|
||||
projects: Project[]
|
||||
}) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Sections"
|
||||
className="sb-rail z-10 w-[236px] shrink-0 overflow-y-auto"
|
||||
>
|
||||
{projects.includes('ecw') && (
|
||||
<>
|
||||
<SectionHead project="ecw" first={projects[0] === 'ecw'} />
|
||||
{/* First, and deliberately not a drop target: ROI is a report over the
|
||||
whole pipeline, not a place a batch can be put. Passing no
|
||||
`dropStage` is what makes dropping onto it impossible, rather than
|
||||
a handler that quietly does nothing. */}
|
||||
{isAdmin && (
|
||||
<Item
|
||||
tab="roi"
|
||||
icon={IconSpend}
|
||||
label="ROI Dashboard"
|
||||
hint="What the pipeline has cost, and what that bought"
|
||||
active={tab === 'roi'}
|
||||
onSelect={onTab}
|
||||
/>
|
||||
)}
|
||||
<Item
|
||||
tab="extraction"
|
||||
dropStage="extraction"
|
||||
dropTab="extraction"
|
||||
icon={IconPage}
|
||||
label="Charge Extraction"
|
||||
hint="Drop PDFs, run the extractor, clear the flagged fields"
|
||||
active={tab === 'extraction'}
|
||||
n={counts.ecw.extraction}
|
||||
onSelect={onTab}
|
||||
onDropBatch={onDropBatch}
|
||||
onDropRun={onDropRun}
|
||||
/>
|
||||
{/* Demography's own spend, apart from the charges ROI above (ticket
|
||||
05). Admin only, the same rule as the other ROI items. */}
|
||||
{isAdmin && (
|
||||
<Item
|
||||
tab="demography-roi"
|
||||
icon={IconSpend}
|
||||
label="Demography ROI"
|
||||
hint="What demography extraction has cost, and what that bought"
|
||||
active={tab === 'demography-roi'}
|
||||
onSelect={onTab}
|
||||
/>
|
||||
)}
|
||||
<Item
|
||||
tab="demography"
|
||||
icon={IconIdCard}
|
||||
label="Demography Extraction"
|
||||
hint="OCR-only extraction for facesheets and admission records"
|
||||
active={tab === 'demography'}
|
||||
onSelect={onTab}
|
||||
/>
|
||||
<Item
|
||||
tab="automation"
|
||||
dropStage="automation"
|
||||
dropTab="automation"
|
||||
icon={IconLoop}
|
||||
label="Charge Automation"
|
||||
hint="Batches that finished extraction and are ready to post"
|
||||
active={tab === 'automation'}
|
||||
n={counts.ecw.automation}
|
||||
onSelect={onTab}
|
||||
onDropBatch={onDropBatch}
|
||||
onDropRun={onDropRun}
|
||||
/>
|
||||
{/* Not a drop target, same reasoning as Demography Extraction above:
|
||||
a demography run is not a batch, so BATCH_DRAG_TYPE's payload
|
||||
(a batch_id) has nowhere to go here. */}
|
||||
<Item
|
||||
tab="demography-automation"
|
||||
icon={IconIdCard}
|
||||
label="Demography Automation"
|
||||
hint="Demography runs that finished and are ready to post"
|
||||
active={tab === 'demography-automation'}
|
||||
onSelect={onTab}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{projects.includes('tebra') && (
|
||||
<>
|
||||
<SectionHead project="tebra" first={projects[0] === 'tebra'} />
|
||||
{isAdmin && (
|
||||
<Item
|
||||
tab="tebra-roi"
|
||||
icon={IconSpend}
|
||||
label="ROI Dashboard"
|
||||
hint="What the Example Client pipeline has cost, and what that bought"
|
||||
active={tab === 'tebra-roi'}
|
||||
onSelect={onTab}
|
||||
/>
|
||||
)}
|
||||
<Item
|
||||
tab="tebra-extraction"
|
||||
dropStage="extraction"
|
||||
dropTab="tebra-extraction"
|
||||
icon={IconPage}
|
||||
label="Example Client Charge Extraction"
|
||||
hint="Example Client tickets: extract and clear the flagged fields"
|
||||
active={tab === 'tebra-extraction'}
|
||||
n={counts.tebra.extraction}
|
||||
onSelect={onTab}
|
||||
onDropBatch={onDropBatch}
|
||||
onDropRun={onDropRun}
|
||||
/>
|
||||
<Item
|
||||
tab="tebra-automation"
|
||||
dropStage="automation"
|
||||
dropTab="tebra-automation"
|
||||
icon={IconLoop}
|
||||
label="Example Client Charge Automation"
|
||||
hint="Example Client batches that finished extraction and are ready to post"
|
||||
active={tab === 'tebra-automation'}
|
||||
n={counts.tebra.automation}
|
||||
onSelect={onTab}
|
||||
onDropBatch={onDropBatch}
|
||||
onDropRun={onDropRun}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* One People entry for the whole app, below every project section
|
||||
rather than inside one — access is an account-level thing, not a
|
||||
board, and a copy under each project only ever asked "which of these
|
||||
two identical links did I mean". The screen itself is merged for
|
||||
anyone who can see both projects (see UsersPanel); a single-project
|
||||
admin lands on their own project's screen, which is why the tab id
|
||||
follows `projects` rather than being fixed — 'people' maps to ECW,
|
||||
and a Tebra-only admin must not be sent to a board they cannot see
|
||||
(App.tsx bounces them straight back off it).
|
||||
|
||||
Not a drop target: this is settings. Admins need it to grant access;
|
||||
operators have nothing to do here and the endpoint would refuse them
|
||||
anyway. */}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<hr className="sb-rail-divider" />
|
||||
<Item
|
||||
tab={projects.includes('ecw') ? 'people' : 'tebra-people'}
|
||||
icon={IconPatients}
|
||||
label="User Access"
|
||||
hint="Who can sign in, and what each of them may do"
|
||||
active={tab === 'people' || tab === 'tebra-people'}
|
||||
onSelect={onTab}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
/** One project's name above its own nav list, with the bar glyph the mockup
|
||||
* uses for a group header. The divider is skipped on the first section —
|
||||
* there is nothing yet to divide it from. */
|
||||
function SectionHead({ project, first }: { project: Project; first: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{!first && <hr className="sb-rail-divider" />}
|
||||
<div className="sb-rail-group">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
||||
<path d="M5 20V11M12 20V5M19 20V14" />
|
||||
</svg>
|
||||
<span className="whitespace-pre-line">{PROJECT_LABEL[project]}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const BATCH_DRAG_TYPE = 'application/x-superbill-batch'
|
||||
const RUN_DRAG_TYPE = 'application/x-superbill-run'
|
||||
|
||||
function Item({
|
||||
tab,
|
||||
dropStage,
|
||||
dropTab,
|
||||
icon: IconEl,
|
||||
label,
|
||||
hint,
|
||||
active,
|
||||
n,
|
||||
onSelect,
|
||||
onDropBatch,
|
||||
onDropRun,
|
||||
}: {
|
||||
tab: BoardTab
|
||||
/** The stage a card dropped here should move to. Absent on sections that
|
||||
* are a view rather than a place, which is what makes them reject a drop. */
|
||||
dropStage?: BatchStage
|
||||
/** The board to navigate to after a drop. Two projects can share a stage
|
||||
* name (both have an "extraction" and an "automation"), so the stage alone
|
||||
* no longer identifies which board a card landed on — this does. Defaults
|
||||
* to `tab`, which was the drop target anyway on every item that has one. */
|
||||
dropTab?: BoardTab
|
||||
icon: (p: { size?: number; className?: string }) => ReactElement
|
||||
label: string
|
||||
hint: string
|
||||
active: boolean
|
||||
/** Omitted on sections that count nothing — a report has no queue depth. */
|
||||
n?: number
|
||||
onSelect: (t: BoardTab) => void
|
||||
onDropBatch?: (batchId: string, stage: BatchStage) => void
|
||||
onDropRun?: (jobId: string) => void
|
||||
}) {
|
||||
const [over, setOver] = useState(false)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(tab)}
|
||||
title={hint}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
// A section is also a drop target, so a batch — or a single document —
|
||||
// can be thrown at "Automation" without first switching to it and
|
||||
// losing sight of the card being dragged.
|
||||
onDragOver={(e) => {
|
||||
if (!dropStage) return
|
||||
const t = e.dataTransfer.types
|
||||
if (t.includes(BATCH_DRAG_TYPE) || (onDropRun && t.includes(RUN_DRAG_TYPE))) {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
setOver(true)
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => setOver(false)}
|
||||
onDrop={(e) => {
|
||||
if (!dropStage) return
|
||||
setOver(false)
|
||||
const batchId = e.dataTransfer.getData(BATCH_DRAG_TYPE)
|
||||
const runId = e.dataTransfer.getData(RUN_DRAG_TYPE)
|
||||
if (!batchId && !runId) return
|
||||
e.preventDefault()
|
||||
if (batchId && onDropBatch) onDropBatch(batchId, dropStage)
|
||||
else if (runId && onDropRun) onDropRun(runId)
|
||||
// Navigate to the board that was dropped ON, not to the stage name —
|
||||
// see the `dropTab` prop doc above.
|
||||
onSelect(dropTab ?? tab)
|
||||
}}
|
||||
className={`sb-nav-item ${over ? 'bg-surface-2 text-ink-2' : ''}`}
|
||||
>
|
||||
<IconEl size={16} className="shrink-0" />
|
||||
<span className="truncate">{label}</span>
|
||||
{n !== undefined && (
|
||||
// Red only when the number is work somebody has to pick up. A badge
|
||||
// that is red at zero teaches people to stop reading it.
|
||||
<span className={`sb-count ${n > 0 ? 'sb-count-hot' : ''}`}>{n}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user