@aglyn/plugins-crm 1.0.0-beta.293 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,8 +21,9 @@ import { MdiIcon } from "@aglyn/shared-ui-jsx";
21
21
  import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
22
22
  import { closestCorners, DndContext, DragOverlay, KeyboardSensor, PointerSensor, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
23
23
  import { CSS } from "@dnd-kit/utilities";
24
- import { Box, Button, Chip, CircularProgress, Paper, Stack, Typography } from "@mui/material";
24
+ import { Box, Button, Chip, CircularProgress, Paper, Stack, TabScrollButton, Tooltip, Typography } from "@mui/material";
25
25
  import { useCallback, useMemo, useState } from "react";
26
+ import { useScrollEdges } from "../hooks/use-scroll-edges.js";
26
27
  import { daysInStage, formatMoney, openStages } from "../model/deal-board-model.js";
27
28
  import { OwnerAvatar } from "./owner-avatar.js";
28
29
  /** The droppable id a stage column answers to. */ const columnId = (stageId)=>`stage:${stageId}`;
@@ -54,11 +55,23 @@ const stageOfColumn = (id)=>typeof id === 'string' && id.startsWith('stage:') ?
54
55
  * two closed columns start collapsed to a count-free stub and expand on
55
56
  * request, which is also when their rows are read — a collapsed column
56
57
  * costs no listener.
58
+ *
59
+ * ## Empty stages fold too, and the board says when there is more
60
+ *
61
+ * A pipeline with Salesforce's ten stages is wider than the card, and the
62
+ * stages with deals in them were off the edge with nothing to say so. An
63
+ * open stage with no deals folds to the same narrow stub, labelled and still
64
+ * a drop target, so the stages that hold work get the room. When the row is
65
+ * still wider than the card, chevrons at either end page it sideways the way
66
+ * MUI's scrollable Tabs do — the scrollbar alone stays hidden on a trackpad
67
+ * until the reader already knows to scroll.
57
68
  */ export function DealBoard(props) {
58
69
  const { pipeline, deals, won, lost, closedExpanded, onToggleClosed, labelFor, moving, onOpen, onMove, onWon, onLost, nowMs } = props;
59
70
  const stages = useMemo(()=>openStages(pipeline), [
60
71
  pipeline
61
72
  ]);
73
+ const edges = useScrollEdges();
74
+ const overflowing = edges.start || edges.end;
62
75
  const byStage = useMemo(()=>{
63
76
  const map = new Map();
64
77
  for (const stage of stages)map.set(stage.id, []);
@@ -165,70 +178,98 @@ const stageOfColumn = (id)=>typeof id === 'string' && id.startsWith('stage:') ?
165
178
  onDragEnd: handleDragEnd,
166
179
  onDragCancel: ()=>setDragging(null),
167
180
  children: [
168
- /*#__PURE__*/ _jsx(Box, {
181
+ /*#__PURE__*/ _jsxs(Stack, {
182
+ direction: "row",
169
183
  sx: {
170
- overflowX: 'auto',
171
- pb: 1,
184
+ alignItems: 'stretch',
172
185
  minWidth: 0,
173
186
  width: '100%'
174
187
  },
175
- children: /*#__PURE__*/ _jsxs(Stack, {
176
- direction: "row",
177
- spacing: 1.5,
178
- sx: {
179
- alignItems: 'stretch',
180
- minWidth: 'max-content'
181
- },
182
- children: [
183
- stages.map((stage)=>{
184
- var _ref, _byStage_map_get, _byStage_map_get1;
185
- var _byStage_map_get2, _byStage_map_get_, _byStage_map_get3;
186
- return /*#__PURE__*/ _jsx(StageColumn, {
187
- stage: stage,
188
- count: (_ref = (_byStage_map_get2 = byStage.map.get(stage.id)) == null ? void 0 : _byStage_map_get2.length) != null ? _ref : 0,
189
- total: ((_byStage_map_get = byStage.map.get(stage.id)) != null ? _byStage_map_get : []).reduce((sum, deal)=>sum + (Number(deal.amountCents) || 0), 0),
190
- currency: (_byStage_map_get3 = byStage.map.get(stage.id)) == null ? void 0 : (_byStage_map_get_ = _byStage_map_get3[0]) == null ? void 0 : _byStage_map_get_.currency,
191
- children: ((_byStage_map_get1 = byStage.map.get(stage.id)) != null ? _byStage_map_get1 : []).map(renderCard)
192
- }, stage.id);
193
- }),
194
- byStage.orphans.length ? /*#__PURE__*/ _jsx(StageColumn, {
195
- stage: {
196
- id: '__orphans',
197
- name: 'No stage',
198
- order: Number.MAX_SAFE_INTEGER,
199
- probability: 0,
200
- kind: 'open'
188
+ children: [
189
+ overflowing ? /*#__PURE__*/ _jsx(TabScrollButton, {
190
+ direction: "left",
191
+ orientation: "horizontal",
192
+ disabled: !edges.start,
193
+ onClick: ()=>edges.page(-1),
194
+ "aria-label": "Show earlier stages",
195
+ "data-testid": "deal-board-scroll-start"
196
+ }) : null,
197
+ /*#__PURE__*/ _jsx(Box, {
198
+ ref: edges.ref,
199
+ sx: {
200
+ overflowX: 'auto',
201
+ pb: 1,
202
+ minWidth: 0,
203
+ flex: 1
204
+ },
205
+ children: /*#__PURE__*/ _jsxs(Stack, {
206
+ direction: "row",
207
+ spacing: 1.5,
208
+ sx: {
209
+ alignItems: 'stretch',
210
+ minWidth: 'max-content'
201
211
  },
202
- count: byStage.orphans.length,
203
- total: 0,
204
- currency: undefined,
205
- droppable: false,
206
- caption: "These deals sit in a stage the pipeline no longer has. Move them.",
207
- children: byStage.orphans.map(renderCard)
208
- }) : null,
209
- closedStages.map((stage)=>{
210
- var _ref;
211
- const rows = stage.kind === 'won' ? won : lost;
212
- return /*#__PURE__*/ _jsx(StageColumn, {
213
- stage: stage,
214
- count: (_ref = rows == null ? void 0 : rows.length) != null ? _ref : null,
215
- total: null,
216
- currency: undefined,
217
- collapsed: !closedExpanded,
218
- onToggle: onToggleClosed,
219
- children: closedExpanded && rows === null ? /*#__PURE__*/ _jsx(Stack, {
220
- sx: {
221
- alignItems: 'center',
222
- py: 2
212
+ children: [
213
+ stages.map((stage)=>{
214
+ var _ref, _byStage_map_get, _byStage_map_get1;
215
+ var _byStage_map_get2, _byStage_map_get3, _byStage_map_get_, _byStage_map_get4;
216
+ return /*#__PURE__*/ _jsx(StageColumn, {
217
+ stage: stage,
218
+ folded: !((_byStage_map_get2 = byStage.map.get(stage.id)) == null ? void 0 : _byStage_map_get2.length),
219
+ count: (_ref = (_byStage_map_get3 = byStage.map.get(stage.id)) == null ? void 0 : _byStage_map_get3.length) != null ? _ref : 0,
220
+ total: ((_byStage_map_get = byStage.map.get(stage.id)) != null ? _byStage_map_get : []).reduce((sum, deal)=>sum + (Number(deal.amountCents) || 0), 0),
221
+ currency: (_byStage_map_get4 = byStage.map.get(stage.id)) == null ? void 0 : (_byStage_map_get_ = _byStage_map_get4[0]) == null ? void 0 : _byStage_map_get_.currency,
222
+ children: ((_byStage_map_get1 = byStage.map.get(stage.id)) != null ? _byStage_map_get1 : []).map(renderCard)
223
+ }, stage.id);
224
+ }),
225
+ byStage.orphans.length ? /*#__PURE__*/ _jsx(StageColumn, {
226
+ stage: {
227
+ id: '__orphans',
228
+ name: 'No stage',
229
+ order: Number.MAX_SAFE_INTEGER,
230
+ probability: 0,
231
+ kind: 'open'
223
232
  },
224
- children: /*#__PURE__*/ _jsx(CircularProgress, {
225
- size: 20
226
- })
227
- }) : (rows != null ? rows : []).map(renderCard)
228
- }, stage.id);
233
+ count: byStage.orphans.length,
234
+ total: 0,
235
+ currency: undefined,
236
+ droppable: false,
237
+ caption: "These deals sit in a stage the pipeline no longer has. Move them.",
238
+ children: byStage.orphans.map(renderCard)
239
+ }) : null,
240
+ closedStages.map((stage)=>{
241
+ var _ref;
242
+ const rows = stage.kind === 'won' ? won : lost;
243
+ return /*#__PURE__*/ _jsx(StageColumn, {
244
+ stage: stage,
245
+ count: (_ref = rows == null ? void 0 : rows.length) != null ? _ref : null,
246
+ total: null,
247
+ currency: undefined,
248
+ collapsed: !closedExpanded,
249
+ onToggle: onToggleClosed,
250
+ children: closedExpanded && rows === null ? /*#__PURE__*/ _jsx(Stack, {
251
+ sx: {
252
+ alignItems: 'center',
253
+ py: 2
254
+ },
255
+ children: /*#__PURE__*/ _jsx(CircularProgress, {
256
+ size: 20
257
+ })
258
+ }) : (rows != null ? rows : []).map(renderCard)
259
+ }, stage.id);
260
+ })
261
+ ]
229
262
  })
230
- ]
231
- })
263
+ }),
264
+ overflowing ? /*#__PURE__*/ _jsx(TabScrollButton, {
265
+ direction: "right",
266
+ orientation: "horizontal",
267
+ disabled: !edges.end,
268
+ onClick: ()=>edges.page(1),
269
+ "aria-label": "Show later stages",
270
+ "data-testid": "deal-board-scroll-end"
271
+ }) : null
272
+ ]
232
273
  }),
233
274
  /*#__PURE__*/ _jsx(DragOverlay, {
234
275
  dropAnimation: null,
@@ -244,7 +285,7 @@ const stageOfColumn = (id)=>typeof id === 'string' && id.startsWith('stage:') ?
244
285
  }
245
286
  DealBoard.displayName = 'DealBoard';
246
287
  function StageColumn(props) {
247
- const { stage, count, total, currency, droppable = true, collapsed = false, onToggle, caption, children } = props;
288
+ const { stage, count, total, currency, droppable = true, collapsed = false, folded = false, onToggle, caption, children } = props;
248
289
  const { setNodeRef, isOver } = useDroppable({
249
290
  id: columnId(stage.id),
250
291
  disabled: !droppable || collapsed
@@ -254,7 +295,7 @@ function StageColumn(props) {
254
295
  ref: setNodeRef,
255
296
  variant: "outlined",
256
297
  sx: {
257
- width: collapsed ? 56 : 260,
298
+ width: collapsed || folded ? 56 : 260,
258
299
  flexShrink: 0,
259
300
  display: 'flex',
260
301
  flexDirection: 'column',
@@ -267,7 +308,24 @@ function StageColumn(props) {
267
308
  ]),
268
309
  minHeight: 240
269
310
  },
270
- children: collapsed ? /*#__PURE__*/ _jsx(Button, {
311
+ children: folded && !collapsed ? /*#__PURE__*/ _jsx(Tooltip, {
312
+ title: `${stage.name}: no deals · ${stage.probability}% likely to close`,
313
+ placement: "top",
314
+ children: /*#__PURE__*/ _jsx(Typography, {
315
+ variant: "subtitle2",
316
+ color: "text.secondary",
317
+ "data-testid": "deal-board-folded-stage",
318
+ sx: {
319
+ flex: 1,
320
+ writingMode: 'vertical-rl',
321
+ textOrientation: 'mixed',
322
+ textAlign: 'center',
323
+ py: 2,
324
+ alignSelf: 'center'
325
+ },
326
+ children: stage.name
327
+ })
328
+ }) : collapsed ? /*#__PURE__*/ _jsx(Button, {
271
329
  onClick: onToggle,
272
330
  sx: {
273
331
  flex: 1,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-board.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { CrmDealStage } from '@aglyn/aglyn'\nimport {\n mdiChevronRight,\n mdiOpenInNew,\n mdiThumbDownOutline,\n mdiTrophyOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport {\n closestCorners,\n DndContext,\n type DragEndEvent,\n DragOverlay,\n type DragStartEvent,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n} from '@dnd-kit/core'\nimport { CSS } from '@dnd-kit/utilities'\nimport {\n Box,\n Button,\n Chip,\n CircularProgress,\n Paper,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n daysInStage,\n type DealDoc,\n formatMoney,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport { OwnerAvatar } from './owner-avatar'\n\n/** The droppable id a stage column answers to. */\nconst columnId = (stageId: string) => `stage:${stageId}`\nconst stageOfColumn = (id: unknown) =>\n typeof id === 'string' && id.startsWith('stage:') ? id.slice('stage:'.length) : null\n\nexport interface DealBoardProps {\n pipeline: PipelineDoc\n /** Every open deal in the pipeline; the board sorts them into columns. */\n deals: readonly DealDoc[]\n /** The closed columns' rows once expanded; `null` while collapsed. */\n won: readonly DealDoc[] | null\n lost: readonly DealDoc[] | null\n closedExpanded: boolean\n onToggleClosed: () => void\n /** The owner's name for a uid, from the roster. */\n labelFor: (uid: string | undefined) => string\n /** Deals whose move is in flight — drawn dimmed, not draggable. */\n moving: ReadonlySet<string>\n onOpen: (deal: DealDoc) => void\n onMove: (deal: DealDoc, stageId: string) => void\n onWon: (deal: DealDoc) => void\n onLost: (deal: DealDoc) => void\n nowMs: number\n}\n\n/**\n * The pipeline as columns (AGL-2598): one per open stage, Won and Lost at\n * the end, cards dragged between them.\n *\n * ## Dragging is a request, not a write\n *\n * `@dnd-kit` handles the pointer; when a card is released over another\n * column the board calls `onMove`, and the SECTION calls the stage route.\n * The card is not moved locally: it dims while the request is in flight\n * and lands in its new column when the listener hears the write. An\n * optimistic move that the server refused — no permission, a stage the\n * pipeline lost meanwhile — would have to be animated back, and a card that\n * jumps back with no explanation reads as a bug in the board rather than a\n * refusal by the server, which is what it was.\n *\n * The pointer sensor needs a few pixels of travel before a drag begins, so\n * a click on the title still opens the deal and a click on the menu still\n * opens the menu. The keyboard sensor makes the same move reachable without\n * a mouse; the card menu's \"Move to\" items are the other route.\n *\n * ## Won and Lost fold away\n *\n * A closed deal is history, and a board that drew every won deal of the\n * year beside the four open stages would push the work off the screen. The\n * two closed columns start collapsed to a count-free stub and expand on\n * request, which is also when their rows are read — a collapsed column\n * costs no listener.\n */\nexport function DealBoard(props: DealBoardProps) {\n const {\n pipeline,\n deals,\n won,\n lost,\n closedExpanded,\n onToggleClosed,\n labelFor,\n moving,\n onOpen,\n onMove,\n onWon,\n onLost,\n nowMs,\n } = props\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n const byStage = useMemo(() => {\n const map = new Map<string, DealDoc[]>()\n for (const stage of stages) map.set(stage.id, [])\n const orphans: DealDoc[] = []\n for (const deal of deals) {\n const column = map.get(deal.stageId)\n if (column) column.push(deal)\n else orphans.push(deal)\n }\n return { map, orphans }\n }, [stages, deals])\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n )\n const [dragging, setDragging] = useState<DealDoc | null>(null)\n const handleDragStart = useCallback((event: DragStartEvent) => {\n setDragging((event.active.data.current?.['deal'] as DealDoc) ?? null)\n }, [])\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n setDragging(null)\n const deal = event.active.data.current?.['deal'] as DealDoc | undefined\n const target = stageOfColumn(event.over?.id)\n if (!deal || !target || target === deal.stageId) return\n const stage = pipeline.stages.find((entry) => entry.id === target)\n if (!stage) return\n if (stage.kind === 'won') return onWon(deal)\n if (stage.kind === 'lost') return onLost(deal)\n onMove(deal, target)\n },\n [pipeline, onMove, onWon, onLost],\n )\n\n const cardActions = useCallback(\n (deal: DealDoc): RowActionsMenuItem[] => [\n {\n key: 'open',\n label: 'Open',\n icon: <MdiIcon path={mdiOpenInNew.path} size={0.8} />,\n onClick: () => onOpen(deal),\n },\n ...stages\n .filter((stage) => stage.id !== deal.stageId)\n .map((stage) => ({\n key: `move:${stage.id}`,\n label: `Move to ${stage.name}`,\n icon: <MdiIcon path={mdiChevronRight.path} size={0.8} />,\n onClick: () => onMove(deal, stage.id),\n })),\n {\n key: 'won',\n label: 'Mark won',\n icon: <MdiIcon path={mdiTrophyOutline.path} size={0.8} />,\n onClick: () => onWon(deal),\n },\n {\n key: 'lost',\n label: 'Mark lost',\n icon: <MdiIcon path={mdiThumbDownOutline.path} size={0.8} />,\n onClick: () => onLost(deal),\n destructive: true,\n },\n ],\n [stages, onOpen, onMove, onWon, onLost],\n )\n\n const renderCard = (deal: DealDoc) => (\n <DealCard\n key={deal.$id}\n deal={deal}\n ownerLabel={labelFor(deal.ownerUid)}\n days={daysInStage(deal, nowMs)}\n pending={moving.has(deal.$id)}\n actions={cardActions(deal)}\n onOpen={() => onOpen(deal)}\n />\n )\n\n const closedStages = pipeline.stages.filter((stage) => stage.kind !== 'open')\n\n return (\n <DndContext\n sensors={sensors}\n collisionDetection={closestCorners}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n onDragCancel={() => setDragging(null)}\n >\n {/*\n The board scrolls sideways; the page does not. The columns are\n fixed-width and refuse to shrink, so the row is as wide as its\n stages — and a flex item's implicit `min-width: auto` would hand\n that width up to the card and the page rather than clip it here.\n `minWidth: 0` is what lets this box be narrower than its content,\n which is the whole condition for the scrollbar to appear on it.\n */}\n <Box sx={{ overflowX: 'auto', pb: 1, minWidth: 0, width: '100%' }}>\n <Stack direction=\"row\" spacing={1.5} sx={{ alignItems: 'stretch', minWidth: 'max-content' }}>\n {stages.map((stage) => (\n <StageColumn\n key={stage.id}\n stage={stage}\n count={byStage.map.get(stage.id)?.length ?? 0}\n total={(byStage.map.get(stage.id) ?? []).reduce(\n (sum, deal) => sum + (Number(deal.amountCents) || 0),\n 0,\n )}\n currency={byStage.map.get(stage.id)?.[0]?.currency}\n >\n {(byStage.map.get(stage.id) ?? []).map(renderCard)}\n </StageColumn>\n ))}\n {byStage.orphans.length ? (\n <StageColumn\n stage={{\n id: '__orphans',\n name: 'No stage',\n order: Number.MAX_SAFE_INTEGER,\n probability: 0,\n kind: 'open',\n }}\n count={byStage.orphans.length}\n total={0}\n currency={undefined}\n droppable={false}\n caption=\"These deals sit in a stage the pipeline no longer has. Move them.\"\n >\n {byStage.orphans.map(renderCard)}\n </StageColumn>\n ) : null}\n {closedStages.map((stage) => {\n const rows = stage.kind === 'won' ? won : lost\n return (\n <StageColumn\n key={stage.id}\n stage={stage}\n count={rows?.length ?? null}\n total={null}\n currency={undefined}\n collapsed={!closedExpanded}\n onToggle={onToggleClosed}\n >\n {closedExpanded && rows === null ? (\n <Stack sx={{ alignItems: 'center', py: 2 }}>\n <CircularProgress size={20} />\n </Stack>\n ) : (\n (rows ?? []).map(renderCard)\n )}\n </StageColumn>\n )\n })}\n </Stack>\n </Box>\n <DragOverlay dropAnimation={null}>\n {dragging ? (\n <DealCardBody\n deal={dragging}\n ownerLabel={labelFor(dragging.ownerUid)}\n days={daysInStage(dragging, nowMs)}\n lifted\n />\n ) : null}\n </DragOverlay>\n </DndContext>\n )\n}\nDealBoard.displayName = 'DealBoard'\n\ninterface StageColumnProps {\n stage: CrmDealStage\n /** Cards in the column, or null while a closed column has not loaded. */\n count: number | null\n /** The column's open value in cents, or null for a closed column. */\n total: number | null\n currency: string | undefined\n droppable?: boolean\n collapsed?: boolean\n onToggle?: () => void\n caption?: string\n children?: React.ReactNode\n}\n\nfunction StageColumn(props: StageColumnProps) {\n const {\n stage,\n count,\n total,\n currency,\n droppable = true,\n collapsed = false,\n onToggle,\n caption,\n children,\n } = props\n const { setNodeRef, isOver } = useDroppable({\n id: columnId(stage.id),\n disabled: !droppable || collapsed,\n })\n const closed = stage.kind !== 'open'\n return (\n <Paper\n ref={setNodeRef}\n variant=\"outlined\"\n sx={{\n width: collapsed ? 56 : 260,\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n bgcolor: isOver ? 'action.hover' : 'background.default',\n borderColor: isOver ? 'primary.main' : undefined,\n transition: (theme) => theme.transitions.create(['background-color', 'border-color', 'width']),\n minHeight: 240,\n }}\n >\n {collapsed ? (\n <Button\n onClick={onToggle}\n sx={{\n flex: 1,\n writingMode: 'vertical-rl',\n textOrientation: 'mixed',\n py: 2,\n typography: 'subtitle2',\n }}\n color={stage.kind === 'won' ? 'success' : 'inherit'}\n >\n {stage.name}\n </Button>\n ) : (\n <>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', px: 1.5, py: 1 }}\n >\n <Typography variant=\"subtitle2\" sx={{ flex: 1 }} noWrap>\n {stage.name}\n </Typography>\n {count !== null ? <Chip size=\"small\" label={count} /> : null}\n {closed && onToggle ? (\n <Button size=\"small\" onClick={onToggle}>\n {'Hide'}\n </Button>\n ) : null}\n </Stack>\n {!closed ? (\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ px: 1.5 }}>\n {`${stage.probability}% · ${total ? formatMoney(total, currency) : 'no value yet'}`}\n </Typography>\n ) : null}\n {caption ? (\n <Typography variant=\"caption\" color=\"warning.main\" sx={{ px: 1.5 }}>\n {caption}\n </Typography>\n ) : null}\n <Stack spacing={1} sx={{ p: 1, flex: 1 }}>\n {children}\n </Stack>\n </>\n )}\n </Paper>\n )\n}\nStageColumn.displayName = 'StageColumn'\n\ninterface DealCardProps {\n deal: DealDoc\n ownerLabel: string\n days: number\n pending: boolean\n actions: RowActionsMenuItem[]\n onOpen: () => void\n}\n\nfunction DealCard(props: DealCardProps) {\n const { deal, ownerLabel, days, pending, actions, onOpen } = props\n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: deal.$id,\n data: { deal },\n disabled: pending || deal.status !== 'open',\n })\n return (\n <Box\n ref={setNodeRef}\n sx={{\n transform: CSS.Translate.toString(transform),\n opacity: isDragging ? 0.3 : pending ? 0.5 : 1,\n cursor: deal.status === 'open' && !pending ? 'grab' : 'default',\n outline: 'none',\n '&:focus-visible': {\n borderRadius: 1,\n boxShadow: (theme) => `0 0 0 2px ${theme.palette.primary.main}`,\n },\n }}\n // The sensor's attributes and listeners carry no `sx`, and they come\n // AFTER it so the card's own styles are what the rule sees first.\n {...attributes}\n {...listeners}\n >\n <DealCardBody\n deal={deal}\n ownerLabel={ownerLabel}\n days={days}\n menu={<RowActionsMenu items={actions} label={deal.title} />}\n onOpen={onOpen}\n />\n </Box>\n )\n}\nDealCard.displayName = 'DealCard'\n\ninterface DealCardBodyProps {\n deal: DealDoc\n ownerLabel: string\n days: number\n menu?: React.ReactNode\n onOpen?: () => void\n lifted?: boolean\n}\n\n/**\n * What a card shows: the title, the amount, who it is with, the owner and\n * how long it has sat here. Shared by the card in its column and the copy\n * that follows the pointer, so the two cannot drift.\n */\nexport function DealCardBody(props: DealCardBodyProps) {\n const { deal, ownerLabel, days, menu, onOpen, lifted } = props\n const withWhom = [deal.contactName, deal.companyName].filter(Boolean).join(' · ')\n return (\n <Paper\n elevation={lifted ? 6 : 1}\n sx={{ p: 1.25, width: lifted ? 244 : undefined }}\n >\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'flex-start' }}>\n <Typography\n variant=\"body2\"\n component={onOpen ? 'button' : 'span'}\n onClick={onOpen}\n // The title is the link into the deal, drawn as text: a button\n // element so it is focusable and announced, unstyled so it reads\n // as the card's heading rather than a control beside it.\n sx={{\n flex: 1,\n textAlign: 'left',\n font: 'inherit',\n fontWeight: 'medium',\n color: 'inherit',\n background: 'none',\n border: 0,\n p: 0,\n cursor: onOpen ? 'pointer' : 'inherit',\n '&:hover': onOpen ? { textDecoration: 'underline' } : undefined,\n }}\n >\n {deal.title || 'Untitled deal'}\n </Typography>\n {menu ? (\n <Box\n // The menu must not start a drag, and the sensor listens on the\n // whole card, so pointer-down here stops before it reaches it.\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n sx={{ m: -0.5 }}\n >\n {menu}\n </Box>\n ) : null}\n </Stack>\n <Typography variant=\"subtitle2\" sx={{ mt: 0.5 }}>\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : '—'}\n </Typography>\n {withWhom ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap sx={{ display: 'block' }}>\n {withWhom}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', mt: 1 }}>\n <OwnerAvatar label={ownerLabel} size={22} />\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ flex: 1 }}>\n {deal.status === 'open'\n ? `${days} ${days === 1 ? 'day' : 'days'} in stage`\n : deal.status === 'won'\n ? 'Won'\n : 'Lost'}\n </Typography>\n </Stack>\n </Paper>\n )\n}\nDealCardBody.displayName = 'DealCardBody'\n\nexport default DealBoard\n"],"names":["mdiChevronRight","mdiOpenInNew","mdiThumbDownOutline","mdiTrophyOutline","MdiIcon","RowActionsMenu","closestCorners","DndContext","DragOverlay","KeyboardSensor","PointerSensor","useDraggable","useDroppable","useSensor","useSensors","CSS","Box","Button","Chip","CircularProgress","Paper","Stack","Typography","useCallback","useMemo","useState","daysInStage","formatMoney","openStages","OwnerAvatar","columnId","stageId","stageOfColumn","id","startsWith","slice","length","DealBoard","props","pipeline","deals","won","lost","closedExpanded","onToggleClosed","labelFor","moving","onOpen","onMove","onWon","onLost","nowMs","stages","byStage","map","Map","stage","set","orphans","deal","column","get","push","sensors","activationConstraint","distance","dragging","setDragging","handleDragStart","event","active","data","current","handleDragEnd","target","over","find","entry","kind","cardActions","key","label","icon","path","size","onClick","filter","name","destructive","renderCard","DealCard","ownerLabel","ownerUid","days","pending","has","$id","actions","closedStages","collisionDetection","onDragStart","onDragEnd","onDragCancel","sx","overflowX","pb","minWidth","width","direction","spacing","alignItems","StageColumn","count","total","reduce","sum","Number","amountCents","currency","order","MAX_SAFE_INTEGER","probability","undefined","droppable","caption","rows","collapsed","onToggle","py","dropAnimation","DealCardBody","lifted","displayName","children","setNodeRef","isOver","disabled","closed","ref","variant","flexShrink","display","flexDirection","bgcolor","borderColor","transition","theme","transitions","create","minHeight","flex","writingMode","textOrientation","typography","color","px","noWrap","p","attributes","listeners","transform","isDragging","status","Translate","toString","opacity","cursor","outline","borderRadius","boxShadow","palette","primary","main","menu","items","title","withWhom","contactName","companyName","Boolean","join","elevation","component","textAlign","font","fontWeight","background","border","textDecoration","onPointerDown","stopPropagation","onKeyDown","m","mt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-board.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { CrmDealStage } from '@aglyn/aglyn'\nimport {\n mdiChevronRight,\n mdiOpenInNew,\n mdiThumbDownOutline,\n mdiTrophyOutline,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport {\n closestCorners,\n DndContext,\n type DragEndEvent,\n DragOverlay,\n type DragStartEvent,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n} from '@dnd-kit/core'\nimport { CSS } from '@dnd-kit/utilities'\nimport {\n Box,\n Button,\n Chip,\n CircularProgress,\n Paper,\n Stack,\n TabScrollButton,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useScrollEdges } from '../hooks/use-scroll-edges'\nimport {\n daysInStage,\n type DealDoc,\n formatMoney,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport { OwnerAvatar } from './owner-avatar'\n\n/** The droppable id a stage column answers to. */\nconst columnId = (stageId: string) => `stage:${stageId}`\nconst stageOfColumn = (id: unknown) =>\n typeof id === 'string' && id.startsWith('stage:') ? id.slice('stage:'.length) : null\n\nexport interface DealBoardProps {\n pipeline: PipelineDoc\n /** Every open deal in the pipeline; the board sorts them into columns. */\n deals: readonly DealDoc[]\n /** The closed columns' rows once expanded; `null` while collapsed. */\n won: readonly DealDoc[] | null\n lost: readonly DealDoc[] | null\n closedExpanded: boolean\n onToggleClosed: () => void\n /** The owner's name for a uid, from the roster. */\n labelFor: (uid: string | undefined) => string\n /** Deals whose move is in flight — drawn dimmed, not draggable. */\n moving: ReadonlySet<string>\n onOpen: (deal: DealDoc) => void\n onMove: (deal: DealDoc, stageId: string) => void\n onWon: (deal: DealDoc) => void\n onLost: (deal: DealDoc) => void\n nowMs: number\n}\n\n/**\n * The pipeline as columns (AGL-2598): one per open stage, Won and Lost at\n * the end, cards dragged between them.\n *\n * ## Dragging is a request, not a write\n *\n * `@dnd-kit` handles the pointer; when a card is released over another\n * column the board calls `onMove`, and the SECTION calls the stage route.\n * The card is not moved locally: it dims while the request is in flight\n * and lands in its new column when the listener hears the write. An\n * optimistic move that the server refused — no permission, a stage the\n * pipeline lost meanwhile — would have to be animated back, and a card that\n * jumps back with no explanation reads as a bug in the board rather than a\n * refusal by the server, which is what it was.\n *\n * The pointer sensor needs a few pixels of travel before a drag begins, so\n * a click on the title still opens the deal and a click on the menu still\n * opens the menu. The keyboard sensor makes the same move reachable without\n * a mouse; the card menu's \"Move to\" items are the other route.\n *\n * ## Won and Lost fold away\n *\n * A closed deal is history, and a board that drew every won deal of the\n * year beside the four open stages would push the work off the screen. The\n * two closed columns start collapsed to a count-free stub and expand on\n * request, which is also when their rows are read — a collapsed column\n * costs no listener.\n *\n * ## Empty stages fold too, and the board says when there is more\n *\n * A pipeline with Salesforce's ten stages is wider than the card, and the\n * stages with deals in them were off the edge with nothing to say so. An\n * open stage with no deals folds to the same narrow stub, labelled and still\n * a drop target, so the stages that hold work get the room. When the row is\n * still wider than the card, chevrons at either end page it sideways the way\n * MUI's scrollable Tabs do — the scrollbar alone stays hidden on a trackpad\n * until the reader already knows to scroll.\n */\nexport function DealBoard(props: DealBoardProps) {\n const {\n pipeline,\n deals,\n won,\n lost,\n closedExpanded,\n onToggleClosed,\n labelFor,\n moving,\n onOpen,\n onMove,\n onWon,\n onLost,\n nowMs,\n } = props\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n const edges = useScrollEdges()\n const overflowing = edges.start || edges.end\n const byStage = useMemo(() => {\n const map = new Map<string, DealDoc[]>()\n for (const stage of stages) map.set(stage.id, [])\n const orphans: DealDoc[] = []\n for (const deal of deals) {\n const column = map.get(deal.stageId)\n if (column) column.push(deal)\n else orphans.push(deal)\n }\n return { map, orphans }\n }, [stages, deals])\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n )\n const [dragging, setDragging] = useState<DealDoc | null>(null)\n const handleDragStart = useCallback((event: DragStartEvent) => {\n setDragging((event.active.data.current?.['deal'] as DealDoc) ?? null)\n }, [])\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n setDragging(null)\n const deal = event.active.data.current?.['deal'] as DealDoc | undefined\n const target = stageOfColumn(event.over?.id)\n if (!deal || !target || target === deal.stageId) return\n const stage = pipeline.stages.find((entry) => entry.id === target)\n if (!stage) return\n if (stage.kind === 'won') return onWon(deal)\n if (stage.kind === 'lost') return onLost(deal)\n onMove(deal, target)\n },\n [pipeline, onMove, onWon, onLost],\n )\n\n const cardActions = useCallback(\n (deal: DealDoc): RowActionsMenuItem[] => [\n {\n key: 'open',\n label: 'Open',\n icon: <MdiIcon path={mdiOpenInNew.path} size={0.8} />,\n onClick: () => onOpen(deal),\n },\n ...stages\n .filter((stage) => stage.id !== deal.stageId)\n .map((stage) => ({\n key: `move:${stage.id}`,\n label: `Move to ${stage.name}`,\n icon: <MdiIcon path={mdiChevronRight.path} size={0.8} />,\n onClick: () => onMove(deal, stage.id),\n })),\n {\n key: 'won',\n label: 'Mark won',\n icon: <MdiIcon path={mdiTrophyOutline.path} size={0.8} />,\n onClick: () => onWon(deal),\n },\n {\n key: 'lost',\n label: 'Mark lost',\n icon: <MdiIcon path={mdiThumbDownOutline.path} size={0.8} />,\n onClick: () => onLost(deal),\n destructive: true,\n },\n ],\n [stages, onOpen, onMove, onWon, onLost],\n )\n\n const renderCard = (deal: DealDoc) => (\n <DealCard\n key={deal.$id}\n deal={deal}\n ownerLabel={labelFor(deal.ownerUid)}\n days={daysInStage(deal, nowMs)}\n pending={moving.has(deal.$id)}\n actions={cardActions(deal)}\n onOpen={() => onOpen(deal)}\n />\n )\n\n const closedStages = pipeline.stages.filter((stage) => stage.kind !== 'open')\n\n return (\n <DndContext\n sensors={sensors}\n collisionDetection={closestCorners}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n onDragCancel={() => setDragging(null)}\n >\n {/*\n The board scrolls sideways; the page does not. The columns are\n fixed-width and refuse to shrink, so the row is as wide as its\n stages — and a flex item's implicit `min-width: auto` would hand\n that width up to the card and the page rather than clip it here.\n `minWidth: 0` is what lets this box be narrower than its content,\n which is the whole condition for the scrollbar to appear on it.\n */}\n <Stack direction=\"row\" sx={{ alignItems: 'stretch', minWidth: 0, width: '100%' }}>\n {overflowing ? (\n <TabScrollButton\n direction=\"left\"\n orientation=\"horizontal\"\n disabled={!edges.start}\n onClick={() => edges.page(-1)}\n aria-label=\"Show earlier stages\"\n data-testid=\"deal-board-scroll-start\"\n />\n ) : null}\n <Box ref={edges.ref} sx={{ overflowX: 'auto', pb: 1, minWidth: 0, flex: 1 }}>\n <Stack direction=\"row\" spacing={1.5} sx={{ alignItems: 'stretch', minWidth: 'max-content' }}>\n {stages.map((stage) => (\n <StageColumn\n key={stage.id}\n stage={stage}\n folded={!byStage.map.get(stage.id)?.length}\n count={byStage.map.get(stage.id)?.length ?? 0}\n total={(byStage.map.get(stage.id) ?? []).reduce(\n (sum, deal) => sum + (Number(deal.amountCents) || 0),\n 0,\n )}\n currency={byStage.map.get(stage.id)?.[0]?.currency}\n >\n {(byStage.map.get(stage.id) ?? []).map(renderCard)}\n </StageColumn>\n ))}\n {byStage.orphans.length ? (\n <StageColumn\n stage={{\n id: '__orphans',\n name: 'No stage',\n order: Number.MAX_SAFE_INTEGER,\n probability: 0,\n kind: 'open',\n }}\n count={byStage.orphans.length}\n total={0}\n currency={undefined}\n droppable={false}\n caption=\"These deals sit in a stage the pipeline no longer has. Move them.\"\n >\n {byStage.orphans.map(renderCard)}\n </StageColumn>\n ) : null}\n {closedStages.map((stage) => {\n const rows = stage.kind === 'won' ? won : lost\n return (\n <StageColumn\n key={stage.id}\n stage={stage}\n count={rows?.length ?? null}\n total={null}\n currency={undefined}\n collapsed={!closedExpanded}\n onToggle={onToggleClosed}\n >\n {closedExpanded && rows === null ? (\n <Stack sx={{ alignItems: 'center', py: 2 }}>\n <CircularProgress size={20} />\n </Stack>\n ) : (\n (rows ?? []).map(renderCard)\n )}\n </StageColumn>\n )\n })}\n </Stack>\n </Box>\n {overflowing ? (\n <TabScrollButton\n direction=\"right\"\n orientation=\"horizontal\"\n disabled={!edges.end}\n onClick={() => edges.page(1)}\n aria-label=\"Show later stages\"\n data-testid=\"deal-board-scroll-end\"\n />\n ) : null}\n </Stack>\n <DragOverlay dropAnimation={null}>\n {dragging ? (\n <DealCardBody\n deal={dragging}\n ownerLabel={labelFor(dragging.ownerUid)}\n days={daysInStage(dragging, nowMs)}\n lifted\n />\n ) : null}\n </DragOverlay>\n </DndContext>\n )\n}\nDealBoard.displayName = 'DealBoard'\n\ninterface StageColumnProps {\n stage: CrmDealStage\n /** Cards in the column, or null while a closed column has not loaded. */\n count: number | null\n /** The column's open value in cents, or null for a closed column. */\n total: number | null\n currency: string | undefined\n droppable?: boolean\n collapsed?: boolean\n /**\n * An open stage with no deals, drawn as a narrow labelled stub — still a\n * drop target, unlike a collapsed closed column, so a deal can be dragged\n * into it.\n */\n folded?: boolean\n onToggle?: () => void\n caption?: string\n children?: React.ReactNode\n}\n\nfunction StageColumn(props: StageColumnProps) {\n const {\n stage,\n count,\n total,\n currency,\n droppable = true,\n collapsed = false,\n folded = false,\n onToggle,\n caption,\n children,\n } = props\n const { setNodeRef, isOver } = useDroppable({\n id: columnId(stage.id),\n disabled: !droppable || collapsed,\n })\n const closed = stage.kind !== 'open'\n return (\n <Paper\n ref={setNodeRef}\n variant=\"outlined\"\n sx={{\n width: collapsed || folded ? 56 : 260,\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n bgcolor: isOver ? 'action.hover' : 'background.default',\n borderColor: isOver ? 'primary.main' : undefined,\n transition: (theme) => theme.transitions.create(['background-color', 'border-color', 'width']),\n minHeight: 240,\n }}\n >\n {folded && !collapsed ? (\n <Tooltip title={`${stage.name}: no deals · ${stage.probability}% likely to close`} placement=\"top\">\n <Typography\n variant=\"subtitle2\"\n color=\"text.secondary\"\n data-testid=\"deal-board-folded-stage\"\n sx={{\n flex: 1,\n writingMode: 'vertical-rl',\n textOrientation: 'mixed',\n textAlign: 'center',\n py: 2,\n alignSelf: 'center',\n }}\n >\n {stage.name}\n </Typography>\n </Tooltip>\n ) : collapsed ? (\n <Button\n onClick={onToggle}\n sx={{\n flex: 1,\n writingMode: 'vertical-rl',\n textOrientation: 'mixed',\n py: 2,\n typography: 'subtitle2',\n }}\n color={stage.kind === 'won' ? 'success' : 'inherit'}\n >\n {stage.name}\n </Button>\n ) : (\n <>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', px: 1.5, py: 1 }}\n >\n <Typography variant=\"subtitle2\" sx={{ flex: 1 }} noWrap>\n {stage.name}\n </Typography>\n {count !== null ? <Chip size=\"small\" label={count} /> : null}\n {closed && onToggle ? (\n <Button size=\"small\" onClick={onToggle}>\n {'Hide'}\n </Button>\n ) : null}\n </Stack>\n {!closed ? (\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ px: 1.5 }}>\n {`${stage.probability}% · ${total ? formatMoney(total, currency) : 'no value yet'}`}\n </Typography>\n ) : null}\n {caption ? (\n <Typography variant=\"caption\" color=\"warning.main\" sx={{ px: 1.5 }}>\n {caption}\n </Typography>\n ) : null}\n <Stack spacing={1} sx={{ p: 1, flex: 1 }}>\n {children}\n </Stack>\n </>\n )}\n </Paper>\n )\n}\nStageColumn.displayName = 'StageColumn'\n\ninterface DealCardProps {\n deal: DealDoc\n ownerLabel: string\n days: number\n pending: boolean\n actions: RowActionsMenuItem[]\n onOpen: () => void\n}\n\nfunction DealCard(props: DealCardProps) {\n const { deal, ownerLabel, days, pending, actions, onOpen } = props\n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: deal.$id,\n data: { deal },\n disabled: pending || deal.status !== 'open',\n })\n return (\n <Box\n ref={setNodeRef}\n sx={{\n transform: CSS.Translate.toString(transform),\n opacity: isDragging ? 0.3 : pending ? 0.5 : 1,\n cursor: deal.status === 'open' && !pending ? 'grab' : 'default',\n outline: 'none',\n '&:focus-visible': {\n borderRadius: 1,\n boxShadow: (theme) => `0 0 0 2px ${theme.palette.primary.main}`,\n },\n }}\n // The sensor's attributes and listeners carry no `sx`, and they come\n // AFTER it so the card's own styles are what the rule sees first.\n {...attributes}\n {...listeners}\n >\n <DealCardBody\n deal={deal}\n ownerLabel={ownerLabel}\n days={days}\n menu={<RowActionsMenu items={actions} label={deal.title} />}\n onOpen={onOpen}\n />\n </Box>\n )\n}\nDealCard.displayName = 'DealCard'\n\ninterface DealCardBodyProps {\n deal: DealDoc\n ownerLabel: string\n days: number\n menu?: React.ReactNode\n onOpen?: () => void\n lifted?: boolean\n}\n\n/**\n * What a card shows: the title, the amount, who it is with, the owner and\n * how long it has sat here. Shared by the card in its column and the copy\n * that follows the pointer, so the two cannot drift.\n */\nexport function DealCardBody(props: DealCardBodyProps) {\n const { deal, ownerLabel, days, menu, onOpen, lifted } = props\n const withWhom = [deal.contactName, deal.companyName].filter(Boolean).join(' · ')\n return (\n <Paper\n elevation={lifted ? 6 : 1}\n sx={{ p: 1.25, width: lifted ? 244 : undefined }}\n >\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'flex-start' }}>\n <Typography\n variant=\"body2\"\n component={onOpen ? 'button' : 'span'}\n onClick={onOpen}\n // The title is the link into the deal, drawn as text: a button\n // element so it is focusable and announced, unstyled so it reads\n // as the card's heading rather than a control beside it.\n sx={{\n flex: 1,\n textAlign: 'left',\n font: 'inherit',\n fontWeight: 'medium',\n color: 'inherit',\n background: 'none',\n border: 0,\n p: 0,\n cursor: onOpen ? 'pointer' : 'inherit',\n '&:hover': onOpen ? { textDecoration: 'underline' } : undefined,\n }}\n >\n {deal.title || 'Untitled deal'}\n </Typography>\n {menu ? (\n <Box\n // The menu must not start a drag, and the sensor listens on the\n // whole card, so pointer-down here stops before it reaches it.\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n sx={{ m: -0.5 }}\n >\n {menu}\n </Box>\n ) : null}\n </Stack>\n <Typography variant=\"subtitle2\" sx={{ mt: 0.5 }}>\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : '—'}\n </Typography>\n {withWhom ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap sx={{ display: 'block' }}>\n {withWhom}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', mt: 1 }}>\n <OwnerAvatar label={ownerLabel} size={22} />\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ flex: 1 }}>\n {deal.status === 'open'\n ? `${days} ${days === 1 ? 'day' : 'days'} in stage`\n : deal.status === 'won'\n ? 'Won'\n : 'Lost'}\n </Typography>\n </Stack>\n </Paper>\n )\n}\nDealCardBody.displayName = 'DealCardBody'\n\nexport default DealBoard\n"],"names":["mdiChevronRight","mdiOpenInNew","mdiThumbDownOutline","mdiTrophyOutline","MdiIcon","RowActionsMenu","closestCorners","DndContext","DragOverlay","KeyboardSensor","PointerSensor","useDraggable","useDroppable","useSensor","useSensors","CSS","Box","Button","Chip","CircularProgress","Paper","Stack","TabScrollButton","Tooltip","Typography","useCallback","useMemo","useState","useScrollEdges","daysInStage","formatMoney","openStages","OwnerAvatar","columnId","stageId","stageOfColumn","id","startsWith","slice","length","DealBoard","props","pipeline","deals","won","lost","closedExpanded","onToggleClosed","labelFor","moving","onOpen","onMove","onWon","onLost","nowMs","stages","edges","overflowing","start","end","byStage","map","Map","stage","set","orphans","deal","column","get","push","sensors","activationConstraint","distance","dragging","setDragging","handleDragStart","event","active","data","current","handleDragEnd","target","over","find","entry","kind","cardActions","key","label","icon","path","size","onClick","filter","name","destructive","renderCard","DealCard","ownerLabel","ownerUid","days","pending","has","$id","actions","closedStages","collisionDetection","onDragStart","onDragEnd","onDragCancel","direction","sx","alignItems","minWidth","width","orientation","disabled","page","aria-label","data-testid","ref","overflowX","pb","flex","spacing","StageColumn","folded","count","total","reduce","sum","Number","amountCents","currency","order","MAX_SAFE_INTEGER","probability","undefined","droppable","caption","rows","collapsed","onToggle","py","dropAnimation","DealCardBody","lifted","displayName","children","setNodeRef","isOver","closed","variant","flexShrink","display","flexDirection","bgcolor","borderColor","transition","theme","transitions","create","minHeight","title","placement","color","writingMode","textOrientation","textAlign","alignSelf","typography","px","noWrap","p","attributes","listeners","transform","isDragging","status","Translate","toString","opacity","cursor","outline","borderRadius","boxShadow","palette","primary","main","menu","items","withWhom","contactName","companyName","Boolean","join","elevation","component","font","fontWeight","background","border","textDecoration","onPointerDown","stopPropagation","onKeyDown","m","mt"],"mappings":"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"}
@@ -9,13 +9,17 @@ export interface DealStageCardProps {
9
9
  /**
10
10
  * Where the deal is, and the controls that move it (AGL-2598).
11
11
  *
12
- * A stepper across the open stages — click one to move there — and the two
13
- * closing buttons. Every one of them calls the stage route, never Firestore:
14
- * the move is what an automation listens for, and the route is the only
15
- * writer that emits it. On a closed deal the stepper gives way to the
16
- * verdict — Won or Lost, when, and why — and a way back: reopening is a
17
- * move into an open stage, which the route emits as `dealStageChanged` like
18
- * any other move.
12
+ * A path across the whole pipeline, the way Salesforce draws one: every open
13
+ * stage — click one to move there — and then one closing step, so the stages
14
+ * still ahead of the deal are always in view and the path ends where every
15
+ * deal ends. The closing step reads "Closed" while the deal is open and asks
16
+ * won or lost when chosen, the same two moves as the buttons in the header;
17
+ * once the deal is closed it reads the pipeline's own name for the verdict.
18
+ * Every move calls the stage route, never Firestore: the move is what an
19
+ * automation listens for, and the route is the only writer that emits it.
20
+ * Under a closed deal's path sits the verdict — when, and why — and a way
21
+ * back: reopening is a move into an open stage, which the route emits as
22
+ * `dealStageChanged` like any other move.
19
23
  *
20
24
  * The line under the stepper reads the deal's own probability and forecast
21
25
  * category (AGL-3516) — a move gives it the new stage's, which the route
@@ -19,31 +19,40 @@ import { CRM_FORECAST_CATEGORY_LABELS, dealForecastCategory, dealProbability, pl
19
19
  import { mdiThumbDownOutline, mdiTrophyOutline } from "@aglyn/shared-data-mdi";
20
20
  import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
21
21
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
22
- import { Button, Chip, MenuItem, Stack, Step, StepButton, Stepper, TextField, Typography } from "@mui/material";
22
+ import { Button, Chip, ListItemIcon, ListItemText, Menu, MenuItem, Stack, Step, StepButton, StepLabel, Stepper, TextField, Typography } from "@mui/material";
23
23
  import { useCallback, useState } from "react";
24
- import { daysInStage, DEAL_STATUS_LABELS, openStages } from "../model/deal-board-model.js";
24
+ import { closingStage, daysInStage, DEAL_STATUS_LABELS, openStages } from "../model/deal-board-model.js";
25
25
  import { LostReasonDialog } from "./lost-reason-dialog.js";
26
26
  /**
27
27
  * Where the deal is, and the controls that move it (AGL-2598).
28
28
  *
29
- * A stepper across the open stages — click one to move there — and the two
30
- * closing buttons. Every one of them calls the stage route, never Firestore:
31
- * the move is what an automation listens for, and the route is the only
32
- * writer that emits it. On a closed deal the stepper gives way to the
33
- * verdict — Won or Lost, when, and why — and a way back: reopening is a
34
- * move into an open stage, which the route emits as `dealStageChanged` like
35
- * any other move.
29
+ * A path across the whole pipeline, the way Salesforce draws one: every open
30
+ * stage — click one to move there — and then one closing step, so the stages
31
+ * still ahead of the deal are always in view and the path ends where every
32
+ * deal ends. The closing step reads "Closed" while the deal is open and asks
33
+ * won or lost when chosen, the same two moves as the buttons in the header;
34
+ * once the deal is closed it reads the pipeline's own name for the verdict.
35
+ * Every move calls the stage route, never Firestore: the move is what an
36
+ * automation listens for, and the route is the only writer that emits it.
37
+ * Under a closed deal's path sits the verdict — when, and why — and a way
38
+ * back: reopening is a move into an open stage, which the route emits as
39
+ * `dealStageChanged` like any other move.
36
40
  *
37
41
  * The line under the stepper reads the deal's own probability and forecast
38
42
  * category (AGL-3516) — a move gives it the new stage's, which the route
39
43
  * stamps, and the stepper says so on hover.
40
44
  */ export function DealStageCard(props) {
45
+ var _ref, _ref1;
46
+ var _closingStage, _closingStage1;
41
47
  const { deal, pipeline, api, nowMs } = props;
42
48
  const { enqueueSnackbar } = useSnackbar();
43
49
  const [busy, setBusy] = useState(false);
44
50
  const [losing, setLosing] = useState(false);
51
+ const [closeMenu, setCloseMenu] = useState(null);
45
52
  const stages = openStages(pipeline);
46
53
  const activeIndex = stages.findIndex((stage)=>stage.id === deal.stageId);
54
+ const wonName = (_ref = (_closingStage = closingStage(pipeline, 'won')) == null ? void 0 : _closingStage.name) != null ? _ref : DEAL_STATUS_LABELS.won;
55
+ const lostName = (_ref1 = (_closingStage1 = closingStage(pipeline, 'lost')) == null ? void 0 : _closingStage1.name) != null ? _ref1 : DEAL_STATUS_LABELS.lost;
47
56
  const run = useCallback(async (request, done)=>{
48
57
  setBusy(true);
49
58
  try {
@@ -70,6 +79,44 @@ import { LostReasonDialog } from "./lost-reason-dialog.js";
70
79
  const current = activeIndex === -1 ? null : stages[activeIndex];
71
80
  const probability = dealProbability(deal, current);
72
81
  const forecast = CRM_FORECAST_CATEGORY_LABELS[dealForecastCategory(deal, current)];
82
+ const path = /*#__PURE__*/ _jsxs(Stepper, {
83
+ nonLinear: true,
84
+ activeStep: closed ? stages.length : activeIndex,
85
+ alternativeLabel: true,
86
+ children: [
87
+ stages.map((stage, index)=>/*#__PURE__*/ _jsx(Step, {
88
+ completed: closed || index < activeIndex,
89
+ children: /*#__PURE__*/ _jsx(StepButton, {
90
+ title: closed ? undefined : `Move here: ${stage.probability}% likely to close`,
91
+ disabled: busy || closed || stage.id === deal.stageId,
92
+ onClick: ()=>void run(()=>api.moveToStage(deal, stage.id), `Moved to ${stage.name}`),
93
+ children: stage.name
94
+ })
95
+ }, stage.id)),
96
+ /*#__PURE__*/ _jsx(Step, {
97
+ completed: deal.status === 'won',
98
+ children: /*#__PURE__*/ _jsx(StepButton, {
99
+ "data-testid": "deal-stage-closing-step",
100
+ title: closed ? undefined : 'Close the deal as won or lost',
101
+ disabled: busy || closed,
102
+ onClick: (event)=>setCloseMenu(event.currentTarget),
103
+ icon: deal.status === 'won' ? /*#__PURE__*/ _jsx(MdiIcon, {
104
+ path: mdiTrophyOutline.path,
105
+ color: "success",
106
+ fontSize: "medium"
107
+ }) : deal.status === 'lost' ? /*#__PURE__*/ _jsx(MdiIcon, {
108
+ path: mdiThumbDownOutline.path,
109
+ color: "error",
110
+ fontSize: "medium"
111
+ }) : undefined,
112
+ children: /*#__PURE__*/ _jsx(StepLabel, {
113
+ error: deal.status === 'lost',
114
+ children: deal.status === 'won' ? wonName : deal.status === 'lost' ? lostName : 'Closed'
115
+ })
116
+ })
117
+ }, "$closed")
118
+ ]
119
+ });
73
120
  return /*#__PURE__*/ _jsxs(CardDisplay, {
74
121
  header: 'Stage',
75
122
  help: pluginDocsHelp('deals', {
@@ -117,6 +164,7 @@ import { LostReasonDialog } from "./lost-reason-dialog.js";
117
164
  }) : closed ? /*#__PURE__*/ _jsxs(Stack, {
118
165
  spacing: 1.5,
119
166
  children: [
167
+ path,
120
168
  /*#__PURE__*/ _jsxs(Stack, {
121
169
  direction: "row",
122
170
  spacing: 1,
@@ -169,20 +217,7 @@ import { LostReasonDialog } from "./lost-reason-dialog.js";
169
217
  }) : /*#__PURE__*/ _jsxs(Stack, {
170
218
  spacing: 1.5,
171
219
  children: [
172
- /*#__PURE__*/ _jsx(Stepper, {
173
- nonLinear: true,
174
- activeStep: activeIndex,
175
- alternativeLabel: true,
176
- children: stages.map((stage, index)=>/*#__PURE__*/ _jsx(Step, {
177
- completed: index < activeIndex,
178
- children: /*#__PURE__*/ _jsx(StepButton, {
179
- title: `Move here: ${stage.probability}% likely to close`,
180
- disabled: busy || stage.id === deal.stageId,
181
- onClick: ()=>void run(()=>api.moveToStage(deal, stage.id), `Moved to ${stage.name}`),
182
- children: stage.name
183
- })
184
- }, stage.id))
185
- }),
220
+ path,
186
221
  /*#__PURE__*/ _jsx(Typography, {
187
222
  variant: "body2",
188
223
  color: "text.secondary",
@@ -193,6 +228,47 @@ import { LostReasonDialog } from "./lost-reason-dialog.js";
193
228
  })
194
229
  ]
195
230
  }),
231
+ /*#__PURE__*/ _jsxs(Menu, {
232
+ anchorEl: closeMenu,
233
+ open: Boolean(closeMenu),
234
+ onClose: ()=>setCloseMenu(null),
235
+ children: [
236
+ /*#__PURE__*/ _jsxs(MenuItem, {
237
+ onClick: ()=>{
238
+ setCloseMenu(null);
239
+ void run(()=>api.markWon(deal), 'Deal won');
240
+ },
241
+ children: [
242
+ /*#__PURE__*/ _jsx(ListItemIcon, {
243
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
244
+ path: mdiTrophyOutline.path,
245
+ color: "success"
246
+ })
247
+ }),
248
+ /*#__PURE__*/ _jsx(ListItemText, {
249
+ children: wonName
250
+ })
251
+ ]
252
+ }),
253
+ /*#__PURE__*/ _jsxs(MenuItem, {
254
+ onClick: ()=>{
255
+ setCloseMenu(null);
256
+ setLosing(true);
257
+ },
258
+ children: [
259
+ /*#__PURE__*/ _jsx(ListItemIcon, {
260
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
261
+ path: mdiThumbDownOutline.path,
262
+ color: "error"
263
+ })
264
+ }),
265
+ /*#__PURE__*/ _jsx(ListItemText, {
266
+ children: lostName
267
+ })
268
+ ]
269
+ })
270
+ ]
271
+ }),
196
272
  /*#__PURE__*/ _jsx(LostReasonDialog, {
197
273
  open: losing,
198
274
  dealTitle: deal.title,