Initial commit: plan, spec, tickets and dashboard skill templates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-16 13:33:03 +05:30
commit 49f8f49b65
39 files changed
+10736

No files matched your search

@@ -0,0 +1,300 @@
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
* / Gulf Coast), 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 Gulf Coast 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="Gulf Coast Charge Extraction"
hint="Gulf Coast 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="Gulf Coast Charge Automation"
hint="Gulf Coast 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>
)
}