All files / web/src/components/create/abacus TwoStageHandoffCard.tsx

98.59% Statements 211/214
81.48% Branches 22/27
100% Functions 3/3
98.59% Lines 211/214

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 2151x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 36x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 5x 4x 9x 9x 9x   9x 9x 9x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 2x 2x 2x 2x 13x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 8x 2x 2x 2x 2x 6x 2x 4x 4x 4x 4x 1x 3x   4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 16x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x   4x 1x 3x 4x 4x 4x 4x 4x 4x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x  
// Abacus Studio — the Stage A → Stage B hand-off (Gitea #38 / things-haunt-house #456).
//
// One card for the whole life of a two-stage feet print, keyed on `HandoffView`:
// what Stage A is doing, the operator's steps between the stages, the Stage B
// submit, and the way out. Pure presentation — the panel owns the record, the
// job roster and the submit; this only renders what `handoffView` decided.
import { button, CARD, STUDIO } from '@/components/studio/theme'
import {
  type HandoffView,
  STAGE_B_VOUCH_DISCLAIMER,
  stageBHandoffSteps,
  TWO_STAGE_VARIANT_COPY,
  type TwoStageVariant,
  UNATTENDED_START_COPY,
} from './two-stage-print'
 
export interface TwoStageHandoffCardProps {
  /** The design the pair of jobs prints — the two-stage record's name. */
  name: string
  /** Which two-stage print this is (Gitea #45) — from the record, never the panel's checkbox. */
  variant?: TwoStageVariant
  view: HandoffView
  /** Stage B can't be submitted right now (the ordinary submit gate, the feet tray, the seam). */
  disabled: boolean
  /** The reason worth words, when there is one; it becomes the button's title. */
  disabledReason: string | null
  /** The Stage B submit is in flight (render + upload). */
  submitting: boolean
  onSubmitStageB: () => void
  /** Submit Stage B on the operator's word about the plate, past the service's ledger
   *  checks. The only way back in when a stage died with good parts on the bed. */
  onVouchStageB: () => void
  /** Drop the record: the panel goes back to the one-job / Stage A choice. */
  onForget: () => void
  /** Opt-in, off by default: the next Stage B is submitted with `chain.startWhenFeedClears`,
   *  so a Stage B parked only on the external spool starts itself when the printer reports
   *  the spool gone. The panel persists it; the card shows it and flips it. */
  unattendedStart: boolean
  onUnattendedStartChange: (on: boolean) => void
}
 
export function TwoStageHandoffCard({
  name,
  variant = 'tpu-floor',
  view,
  disabled,
  disabledReason,
  submitting,
  onSubmitStageB,
  onVouchStageB,
  onForget,
  unattendedStart,
  onUnattendedStartChange,
}: TwoStageHandoffCardProps) {
  // The one control on this card that can make the printer move with nobody pressing
  // anything. Offered wherever a Stage B can be submitted — the plain submit and the
  // vouched one chain the same way — and nowhere else, since it only shapes the NEXT
  // ticket. The consequence sits under the box in both states, not in a tooltip.
  const unattendedToggle = (
    <label
      data-element="unattended-start"
      style={{ display: 'flex', gap: 8, alignItems: 'flex-start', cursor: 'pointer' }}
    >
      <input
        type="checkbox"
        data-action="toggle-unattended-start"
        checked={unattendedStart}
        onChange={(e) => onUnattendedStartChange(e.target.checked)}
        style={{ marginTop: 2 }}
      />
      <span style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
        <span>{UNATTENDED_START_COPY.label}</span>
        <span style={STUDIO.type.note}>
          {unattendedStart ? UNATTENDED_START_COPY.on : UNATTENDED_START_COPY.off}
        </span>
      </span>
    </label>
  )
  // The override, offered in the two places an operator can be standing in front of good
  // parts the ledger has written off: a stage that ended badly, and a chain the service
  // refuses. Deliberately verbose — it trades away every check the service would have run.
  const vouchOffer = (lead: string, tone: 'primary' | 'chip') => (
    <div data-element="stage-b-vouch" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <span style={{ color: STUDIO.color.text2 }}>{lead}</span>
      <ul
        style={{
          margin: 0,
          paddingLeft: 18,
          display: 'flex',
          flexDirection: 'column',
          gap: 3,
          color: STUDIO.color.text2,
        }}
      >
        {STAGE_B_VOUCH_DISCLAIMER.map((line) => (
          <li key={line}>{line}</li>
        ))}
      </ul>
      <button
        type="button"
        data-action="vouch-stage-b"
        onClick={onVouchStageB}
        disabled={disabled}
        title={disabledReason ?? 'Print Stage B onto parts you have checked are on the plate'}
        style={
          tone === 'primary'
            ? button('primary', { disabled })
            : { ...button('chip'), alignSelf: 'flex-start' }
        }
      >
        {submitting
          ? 'Rendering & submitting…'
          : '🖨 The parts are on the plate — print Stage B anyway'}
      </button>
    </div>
  )
  return (
    <div
      data-element="two-stage-handoff"
      data-handoff={view.kind}
      data-variant={variant}
      style={{
        ...CARD,
        // the one card that is a live thread rather than a summary — it keeps the
        // accent edge so the hand-off reads as the thing currently happening
        border: `1px solid ${STUDIO.color.accentBorder}`,
        display: 'flex',
        flexDirection: 'column',
        gap: STUDIO.space.card,
        lineHeight: 1.45,
      }}
    >
      <strong style={STUDIO.type.strong}>Two-stage print — {name}</strong>
      {view.kind === 'stage-a-running' ? (
        <span>
          Stage A (feet) is {view.phase ?? 'not in the job list yet'} — Stage B unlocks when it
          completes.
        </span>
      ) : view.kind === 'stage-a-ended' ? (
        <>
          <span>
            Stage A {view.phase} — so the print service will not chain onto it. That is a statement
            about the job, not about the plate: a stage stopped after its parts were down leaves
            them there, and they are still printable.
          </span>
          {unattendedToggle}
          {vouchOffer(
            "If Stage A's parts are on the plate, say so and Stage B will print onto them.",
            'primary'
          )}
        </>
      ) : view.kind === 'stage-b-open' ? (
        <span>
          Stage B (body) is {view.phase ?? 'on its way to the job list'} — resolve it from its job
          card below.
        </span>
      ) : view.kind === 'done' ? (
        <span>Stage B completed — the abacus is done. Forget this print to start another.</span>
      ) : (
        <>
          <span>
            {view.retry
              ? 'Stage B did not start — it can be submitted again while Stage A is still the last thing on the plate.'
              : variant === 'feet-only'
                ? `Stage A (feet) is done — Stage B ${TWO_STAGE_VARIANT_COPY['feet-only'].stageB}.`
                : 'Stage A (feet) is done.'}{' '}
            Before Stage B:
          </span>
          <ol
            style={{
              margin: 0,
              paddingLeft: 18,
              display: 'flex',
              flexDirection: 'column',
              gap: 3,
              color: STUDIO.color.text2,
            }}
          >
            {stageBHandoffSteps(variant).map((step) => (
              <li key={step}>{step}</li>
            ))}
          </ol>
          {unattendedToggle}
          <button
            type="button"
            data-action="submit-stage-b"
            onClick={onSubmitStageB}
            disabled={disabled}
            title={disabledReason ?? 'Chain the body onto the feet Stage A printed'}
            style={button('primary', { disabled })}
          >
            {submitting
              ? 'Rendering & submitting…'
              : view.retry
                ? '🖨 Submit Stage B again'
                : '🖨 Submit Stage B (body)'}
          </button>
          {vouchOffer(
            'Refused because the chain is broken — something else printed here, or Stage A ended badly? The plate is the only thing that actually matters, and you can see it.',
            'chip'
          )}
        </>
      )}
      <button
        type="button"
        data-action="forget-two-stage"
        onClick={onForget}
        style={{ ...button('chip'), alignSelf: 'flex-start' }}
      >
        Forget this two-stage print
      </button>
    </div>
  )
}