@contractspec/example.crm-pipeline 3.7.6 → 3.7.10

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.
Files changed (130) hide show
  1. package/.turbo/turbo-build.log +45 -42
  2. package/AGENTS.md +51 -33
  3. package/CHANGELOG.md +36 -0
  4. package/README.md +67 -148
  5. package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
  6. package/dist/browser/docs/index.js +1 -1
  7. package/dist/browser/events/contact.event.js +1 -1
  8. package/dist/browser/events/deal.event.js +1 -1
  9. package/dist/browser/events/index.js +3 -3
  10. package/dist/browser/events/task.event.js +1 -1
  11. package/dist/browser/handlers/crm.handlers.js +13 -2
  12. package/dist/browser/handlers/index.js +13 -2
  13. package/dist/browser/index.js +680 -447
  14. package/dist/browser/ui/CrmDashboard.js +574 -352
  15. package/dist/browser/ui/CrmDealCard.js +5 -5
  16. package/dist/browser/ui/CrmPipelineBoard.js +13 -13
  17. package/dist/browser/ui/hooks/index.js +21 -10
  18. package/dist/browser/ui/hooks/useDealList.js +20 -9
  19. package/dist/browser/ui/hooks/useDealMutations.js +1 -1
  20. package/dist/browser/ui/index.js +683 -450
  21. package/dist/browser/ui/modals/CreateDealModal.js +12 -12
  22. package/dist/browser/ui/modals/DealActionsModal.js +21 -21
  23. package/dist/browser/ui/modals/index.js +33 -33
  24. package/dist/browser/ui/renderers/index.js +140 -118
  25. package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
  26. package/dist/browser/ui/renderers/pipeline.renderer.js +108 -97
  27. package/dist/browser/ui/tables/DealListTab.js +390 -0
  28. package/dist/deal/index.d.ts +2 -2
  29. package/dist/docs/crm-pipeline.docblock.js +1 -1
  30. package/dist/docs/index.js +1 -1
  31. package/dist/events/contact.event.js +1 -1
  32. package/dist/events/deal.event.js +1 -1
  33. package/dist/events/index.js +3 -3
  34. package/dist/events/task.event.js +1 -1
  35. package/dist/handlers/crm.handlers.d.ts +2 -0
  36. package/dist/handlers/crm.handlers.js +13 -2
  37. package/dist/handlers/index.d.ts +2 -2
  38. package/dist/handlers/index.js +13 -2
  39. package/dist/index.d.ts +3 -3
  40. package/dist/index.js +680 -447
  41. package/dist/node/docs/crm-pipeline.docblock.js +1 -1
  42. package/dist/node/docs/index.js +1 -1
  43. package/dist/node/events/contact.event.js +1 -1
  44. package/dist/node/events/deal.event.js +1 -1
  45. package/dist/node/events/index.js +3 -3
  46. package/dist/node/events/task.event.js +1 -1
  47. package/dist/node/handlers/crm.handlers.js +13 -2
  48. package/dist/node/handlers/index.js +13 -2
  49. package/dist/node/index.js +680 -447
  50. package/dist/node/ui/CrmDashboard.js +574 -352
  51. package/dist/node/ui/CrmDealCard.js +5 -5
  52. package/dist/node/ui/CrmPipelineBoard.js +13 -13
  53. package/dist/node/ui/hooks/index.js +21 -10
  54. package/dist/node/ui/hooks/useDealList.js +20 -9
  55. package/dist/node/ui/hooks/useDealMutations.js +1 -1
  56. package/dist/node/ui/index.js +683 -450
  57. package/dist/node/ui/modals/CreateDealModal.js +12 -12
  58. package/dist/node/ui/modals/DealActionsModal.js +21 -21
  59. package/dist/node/ui/modals/index.js +33 -33
  60. package/dist/node/ui/renderers/index.js +140 -118
  61. package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
  62. package/dist/node/ui/renderers/pipeline.renderer.js +108 -97
  63. package/dist/node/ui/tables/DealListTab.js +390 -0
  64. package/dist/operations/index.d.ts +1 -1
  65. package/dist/ui/CrmDashboard.js +574 -352
  66. package/dist/ui/CrmDealCard.js +5 -5
  67. package/dist/ui/CrmPipelineBoard.js +13 -13
  68. package/dist/ui/hooks/index.d.ts +2 -2
  69. package/dist/ui/hooks/index.js +21 -10
  70. package/dist/ui/hooks/useDealList.d.ts +8 -2
  71. package/dist/ui/hooks/useDealList.js +20 -9
  72. package/dist/ui/hooks/useDealMutations.d.ts +9 -0
  73. package/dist/ui/hooks/useDealMutations.js +1 -1
  74. package/dist/ui/index.d.ts +3 -3
  75. package/dist/ui/index.js +683 -450
  76. package/dist/ui/modals/CreateDealModal.js +12 -12
  77. package/dist/ui/modals/DealActionsModal.js +21 -21
  78. package/dist/ui/modals/index.js +33 -33
  79. package/dist/ui/renderers/index.d.ts +1 -1
  80. package/dist/ui/renderers/index.js +140 -118
  81. package/dist/ui/renderers/pipeline.markdown.js +13 -2
  82. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  83. package/dist/ui/renderers/pipeline.renderer.js +108 -97
  84. package/dist/ui/tables/DealListTab.d.ts +20 -0
  85. package/dist/ui/tables/DealListTab.js +391 -0
  86. package/dist/ui/tables/DealListTab.smoke.test.d.ts +1 -0
  87. package/package.json +29 -14
  88. package/src/crm-pipeline.feature.ts +86 -86
  89. package/src/deal/deal.enum.ts +8 -8
  90. package/src/deal/deal.operation.ts +255 -255
  91. package/src/deal/deal.schema.ts +92 -92
  92. package/src/deal/deal.test-spec.ts +48 -48
  93. package/src/deal/index.ts +17 -19
  94. package/src/docs/crm-pipeline.docblock.ts +44 -44
  95. package/src/entities/company.entity.ts +52 -52
  96. package/src/entities/contact.entity.ts +67 -67
  97. package/src/entities/deal.entity.ts +134 -134
  98. package/src/entities/index.ts +27 -27
  99. package/src/entities/task.entity.ts +105 -105
  100. package/src/events/contact.event.ts +22 -22
  101. package/src/events/deal.event.ts +77 -77
  102. package/src/events/task.event.ts +19 -19
  103. package/src/example.ts +32 -32
  104. package/src/handlers/crm.handlers.ts +375 -357
  105. package/src/handlers/deal.handlers.ts +179 -179
  106. package/src/handlers/index.ts +18 -19
  107. package/src/handlers/mock-data.ts +167 -167
  108. package/src/index.ts +11 -11
  109. package/src/operations/index.ts +16 -16
  110. package/src/presentations/dashboard.presentation.ts +45 -45
  111. package/src/presentations/pipeline.presentation.ts +90 -90
  112. package/src/seeders/index.ts +26 -26
  113. package/src/shared/overlay-types.ts +23 -23
  114. package/src/ui/CrmDashboard.tsx +210 -279
  115. package/src/ui/CrmDealCard.tsx +64 -64
  116. package/src/ui/CrmPipelineBoard.tsx +105 -105
  117. package/src/ui/hooks/index.ts +3 -3
  118. package/src/ui/hooks/useDealList.ts +113 -85
  119. package/src/ui/hooks/useDealMutations.ts +151 -150
  120. package/src/ui/index.ts +5 -10
  121. package/src/ui/modals/CreateDealModal.tsx +217 -217
  122. package/src/ui/modals/DealActionsModal.tsx +390 -390
  123. package/src/ui/overlays/demo-overlays.ts +43 -43
  124. package/src/ui/renderers/index.ts +4 -3
  125. package/src/ui/renderers/pipeline.markdown.ts +165 -165
  126. package/src/ui/renderers/pipeline.renderer.tsx +17 -16
  127. package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
  128. package/src/ui/tables/DealListTab.tsx +276 -0
  129. package/tsconfig.json +7 -8
  130. package/tsdown.config.js +7 -3
@@ -23,6 +23,13 @@ function rowToDeal(row) {
23
23
  updatedAt: new Date(row.updatedAt)
24
24
  };
25
25
  }
26
+ var DEAL_SORT_COLUMNS = {
27
+ name: "name",
28
+ value: "value",
29
+ status: "status",
30
+ expectedCloseDate: "expectedCloseDate",
31
+ updatedAt: "updatedAt"
32
+ };
26
33
  function createCrmHandlers(db) {
27
34
  async function listDeals(input) {
28
35
  const {
@@ -33,7 +40,9 @@ function createCrmHandlers(db) {
33
40
  ownerId,
34
41
  search,
35
42
  limit = 20,
36
- offset = 0
43
+ offset = 0,
44
+ sortBy = "value",
45
+ sortDirection = "desc"
37
46
  } = input;
38
47
  let whereClause = "WHERE projectId = ?";
39
48
  const params = [projectId];
@@ -61,7 +70,9 @@ function createCrmHandlers(db) {
61
70
  const total = countResult[0]?.count ?? 0;
62
71
  const valueResult = (await db.query(`SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`, params)).rows;
63
72
  const totalValue = valueResult[0]?.total ?? 0;
64
- const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
73
+ const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
74
+ const orderByDirection = sortDirection === "asc" ? "ASC" : "DESC";
75
+ const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
65
76
  return {
66
77
  deals: dealRows.map(rowToDeal),
67
78
  total,
@@ -441,88 +452,6 @@ async function mockGetDealsByStageHandler(input) {
441
452
  async function mockGetPipelineStagesHandler(input) {
442
453
  return MOCK_STAGES.filter((s) => s.pipelineId === input.pipelineId);
443
454
  }
444
- // src/ui/hooks/useDealList.ts
445
- import { useCallback, useEffect, useMemo, useState } from "react";
446
- import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
447
- "use client";
448
- function useDealList(options = {}) {
449
- const { handlers, projectId } = useTemplateRuntime();
450
- const { crm: crm2 } = handlers;
451
- const [data, setData] = useState(null);
452
- const [dealsByStage, setDealsByStage] = useState({});
453
- const [stages, setStages] = useState([]);
454
- const [loading, setLoading] = useState(true);
455
- const [error, setError] = useState(null);
456
- const [page, setPage] = useState(1);
457
- const pipelineId = options.pipelineId ?? "pipeline-1";
458
- const fetchData = useCallback(async () => {
459
- setLoading(true);
460
- setError(null);
461
- try {
462
- const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
463
- crm2.listDeals({
464
- projectId,
465
- pipelineId,
466
- stageId: options.stageId,
467
- status: options.status === "all" ? undefined : options.status,
468
- search: options.search,
469
- limit: options.limit ?? 50,
470
- offset: (page - 1) * (options.limit ?? 50)
471
- }),
472
- crm2.getDealsByStage({ projectId, pipelineId }),
473
- crm2.getPipelineStages({ pipelineId })
474
- ]);
475
- setData(dealsResult);
476
- setDealsByStage(stageDealsResult);
477
- setStages(stagesResult);
478
- } catch (err) {
479
- setError(err instanceof Error ? err : new Error("Unknown error"));
480
- } finally {
481
- setLoading(false);
482
- }
483
- }, [
484
- crm2,
485
- projectId,
486
- pipelineId,
487
- options.stageId,
488
- options.status,
489
- options.search,
490
- options.limit,
491
- page
492
- ]);
493
- useEffect(() => {
494
- fetchData();
495
- }, [fetchData]);
496
- const stats = useMemo(() => {
497
- if (!data)
498
- return null;
499
- const open = data.deals.filter((d) => d.status === "OPEN");
500
- const won = data.deals.filter((d) => d.status === "WON");
501
- const lost = data.deals.filter((d) => d.status === "LOST");
502
- return {
503
- total: data.total,
504
- totalValue: data.totalValue,
505
- openCount: open.length,
506
- openValue: open.reduce((sum, d) => sum + d.value, 0),
507
- wonCount: won.length,
508
- wonValue: won.reduce((sum, d) => sum + d.value, 0),
509
- lostCount: lost.length
510
- };
511
- }, [data]);
512
- return {
513
- data,
514
- dealsByStage,
515
- stages,
516
- loading,
517
- error,
518
- stats,
519
- page,
520
- refetch: fetchData,
521
- nextPage: () => setPage((p) => p + 1),
522
- prevPage: () => page > 1 && setPage((p) => p - 1)
523
- };
524
- }
525
-
526
455
  // src/ui/CrmDealCard.tsx
527
456
  import { jsxDEV } from "react/jsx-dev-runtime";
528
457
  "use client";
@@ -538,7 +467,7 @@ function CrmDealCard({ deal, onClick }) {
538
467
  const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
539
468
  return /* @__PURE__ */ jsxDEV("div", {
540
469
  onClick,
541
- className: "border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md",
470
+ className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
542
471
  role: "button",
543
472
  tabIndex: 0,
544
473
  onKeyDown: (e) => {
@@ -547,22 +476,22 @@ function CrmDealCard({ deal, onClick }) {
547
476
  },
548
477
  children: [
549
478
  /* @__PURE__ */ jsxDEV("h4", {
550
- className: "leading-snug font-medium",
479
+ className: "font-medium leading-snug",
551
480
  children: deal.name
552
481
  }, undefined, false, undefined, this),
553
482
  /* @__PURE__ */ jsxDEV("div", {
554
- className: "text-primary mt-2 text-lg font-semibold",
483
+ className: "mt-2 font-semibold text-lg text-primary",
555
484
  children: formatCurrency(deal.value, deal.currency)
556
485
  }, undefined, false, undefined, this),
557
486
  /* @__PURE__ */ jsxDEV("div", {
558
- className: "text-muted-foreground mt-3 flex items-center justify-between text-xs",
487
+ className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
559
488
  children: [
560
489
  daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
561
490
  className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
562
491
  children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
563
492
  }, undefined, false, undefined, this),
564
493
  /* @__PURE__ */ jsxDEV("span", {
565
- className: `rounded px-1.5 py-0.5 text-xs font-medium ${deal.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
494
+ className: `rounded px-1.5 py-0.5 font-medium text-xs ${deal.status === "WON" ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" : deal.status === "LOST" ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}`,
566
495
  children: deal.status
567
496
  }, undefined, false, undefined, this)
568
497
  ]
@@ -572,7 +501,7 @@ function CrmDealCard({ deal, onClick }) {
572
501
  }
573
502
 
574
503
  // src/ui/CrmPipelineBoard.tsx
575
- import { useState as useState2 } from "react";
504
+ import { useState } from "react";
576
505
  import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
577
506
  "use client";
578
507
  function formatCurrency2(value) {
@@ -588,7 +517,7 @@ function CrmPipelineBoard({
588
517
  onDealClick,
589
518
  onDealMove
590
519
  }) {
591
- const [quickMoveOpen, setQuickMoveOpen] = useState2(null);
520
+ const [quickMoveOpen, setQuickMoveOpen] = useState(null);
592
521
  const sortedStages = [...stages].sort((a, b) => a.position - b.position);
593
522
  const handleQuickMove = (dealId, toStageId) => {
594
523
  onDealMove?.(dealId, toStageId);
@@ -600,10 +529,10 @@ function CrmPipelineBoard({
600
529
  const deals = dealsByStage[stage.id] ?? [];
601
530
  const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
602
531
  return /* @__PURE__ */ jsxDEV2("div", {
603
- className: "bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg",
532
+ className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
604
533
  children: [
605
534
  /* @__PURE__ */ jsxDEV2("div", {
606
- className: "border-border flex items-center justify-between border-b px-3 py-2",
535
+ className: "flex items-center justify-between border-border border-b px-3 py-2",
607
536
  children: [
608
537
  /* @__PURE__ */ jsxDEV2("div", {
609
538
  children: [
@@ -622,7 +551,7 @@ function CrmPipelineBoard({
622
551
  ]
623
552
  }, undefined, true, undefined, this),
624
553
  /* @__PURE__ */ jsxDEV2("span", {
625
- className: "bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium",
554
+ className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
626
555
  children: deals.length
627
556
  }, undefined, false, undefined, this)
628
557
  ]
@@ -630,7 +559,7 @@ function CrmPipelineBoard({
630
559
  /* @__PURE__ */ jsxDEV2("div", {
631
560
  className: "flex flex-1 flex-col gap-2 p-2",
632
561
  children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
633
- className: "border-muted-foreground/20 text-muted-foreground flex h-24 items-center justify-center rounded-md border-2 border-dashed text-xs",
562
+ className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
634
563
  children: "No deals"
635
564
  }, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
636
565
  className: "group relative",
@@ -648,15 +577,15 @@ function CrmPipelineBoard({
648
577
  e.stopPropagation();
649
578
  setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
650
579
  },
651
- className: "bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm",
580
+ className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
652
581
  title: "Quick move",
653
582
  children: "\u27A1\uFE0F"
654
583
  }, undefined, false, undefined, this),
655
584
  quickMoveOpen === deal.id && /* @__PURE__ */ jsxDEV2("div", {
656
- className: "bg-card border-border absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border py-1 shadow-lg",
585
+ className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
657
586
  children: [
658
587
  /* @__PURE__ */ jsxDEV2("p", {
659
- className: "text-muted-foreground px-3 py-1 text-xs font-medium",
588
+ className: "px-3 py-1 font-medium text-muted-foreground text-xs",
660
589
  children: "Move to:"
661
590
  }, undefined, false, undefined, this),
662
591
  sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
@@ -665,7 +594,7 @@ function CrmPipelineBoard({
665
594
  e.stopPropagation();
666
595
  handleQuickMove(deal.id, s.id);
667
596
  },
668
- className: "hover:bg-muted w-full px-3 py-1.5 text-left text-sm",
597
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
669
598
  children: s.name
670
599
  }, s.id, false, undefined, this))
671
600
  ]
@@ -681,27 +610,98 @@ function CrmPipelineBoard({
681
610
  }, undefined, false, undefined, this);
682
611
  }
683
612
 
684
- // src/ui/renderers/pipeline.renderer.tsx
685
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
686
- function CrmPipelineBoardWrapper() {
687
- const { dealsByStage, stages } = useDealList();
688
- return /* @__PURE__ */ jsxDEV3(CrmPipelineBoard, {
613
+ // src/ui/hooks/useDealList.ts
614
+ import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
615
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
616
+ "use client";
617
+ function useDealList(options = {}) {
618
+ const { handlers, projectId } = useTemplateRuntime();
619
+ const { crm: crm2 } = handlers;
620
+ const [data, setData] = useState2(null);
621
+ const [dealsByStage, setDealsByStage] = useState2({});
622
+ const [stages, setStages] = useState2([]);
623
+ const [loading, setLoading] = useState2(true);
624
+ const [error, setError] = useState2(null);
625
+ const [internalPage, setInternalPage] = useState2(0);
626
+ const pipelineId = options.pipelineId ?? "pipeline-1";
627
+ const pageIndex = options.pageIndex ?? internalPage;
628
+ const pageSize = options.pageSize ?? options.limit ?? 50;
629
+ const [sort] = options.sorting ?? [];
630
+ const sortBy = sort?.id;
631
+ const sortDirection = sort ? sort.desc ? "desc" : "asc" : undefined;
632
+ const fetchData = useCallback(async () => {
633
+ setLoading(true);
634
+ setError(null);
635
+ try {
636
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
637
+ crm2.listDeals({
638
+ projectId,
639
+ pipelineId,
640
+ stageId: options.stageId,
641
+ status: options.status === "all" ? undefined : options.status,
642
+ search: options.search,
643
+ limit: pageSize,
644
+ offset: pageIndex * pageSize,
645
+ sortBy: sortBy === "name" || sortBy === "value" || sortBy === "status" || sortBy === "expectedCloseDate" || sortBy === "updatedAt" ? sortBy : undefined,
646
+ sortDirection
647
+ }),
648
+ crm2.getDealsByStage({ projectId, pipelineId }),
649
+ crm2.getPipelineStages({ pipelineId })
650
+ ]);
651
+ setData(dealsResult);
652
+ setDealsByStage(stageDealsResult);
653
+ setStages(stagesResult);
654
+ } catch (err) {
655
+ setError(err instanceof Error ? err : new Error("Unknown error"));
656
+ } finally {
657
+ setLoading(false);
658
+ }
659
+ }, [
660
+ crm2,
661
+ projectId,
662
+ pipelineId,
663
+ options.stageId,
664
+ options.status,
665
+ options.search,
666
+ pageIndex,
667
+ pageSize,
668
+ sortBy,
669
+ sortDirection
670
+ ]);
671
+ useEffect(() => {
672
+ fetchData();
673
+ }, [fetchData]);
674
+ const stats = useMemo(() => {
675
+ if (!data)
676
+ return null;
677
+ const open = data.deals.filter((d) => d.status === "OPEN");
678
+ const won = data.deals.filter((d) => d.status === "WON");
679
+ const lost = data.deals.filter((d) => d.status === "LOST");
680
+ return {
681
+ total: data.total,
682
+ totalValue: data.totalValue,
683
+ openCount: open.length,
684
+ openValue: open.reduce((sum, d) => sum + d.value, 0),
685
+ wonCount: won.length,
686
+ wonValue: won.reduce((sum, d) => sum + d.value, 0),
687
+ lostCount: lost.length
688
+ };
689
+ }, [data]);
690
+ return {
691
+ data,
689
692
  dealsByStage,
690
- stages
691
- }, undefined, false, undefined, this);
693
+ stages,
694
+ loading,
695
+ error,
696
+ stats,
697
+ page: pageIndex + 1,
698
+ pageIndex,
699
+ pageSize,
700
+ refetch: fetchData,
701
+ nextPage: options.pageIndex === undefined ? () => setInternalPage((page) => page + 1) : undefined,
702
+ prevPage: options.pageIndex === undefined ? () => pageIndex > 0 && setInternalPage((page) => page - 1) : undefined
703
+ };
692
704
  }
693
- var crmPipelineReactRenderer = {
694
- target: "react",
695
- render: async (desc, _ctx) => {
696
- if (desc.source.type !== "component") {
697
- throw new Error("Invalid source type");
698
- }
699
- if (desc.source.componentKey !== "CrmPipelineView") {
700
- throw new Error(`Unknown component: ${desc.source.componentKey}`);
701
- }
702
- return /* @__PURE__ */ jsxDEV3(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
703
- }
704
- };
705
705
 
706
706
  // src/ui/renderers/pipeline.markdown.ts
707
707
  function formatCurrency3(value, currency = "USD") {
@@ -821,6 +821,28 @@ var crmDashboardMarkdownRenderer = {
821
821
  };
822
822
  }
823
823
  };
824
+
825
+ // src/ui/renderers/pipeline.renderer.tsx
826
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
827
+ function CrmPipelineBoardWrapper() {
828
+ const { dealsByStage, stages } = useDealList();
829
+ return /* @__PURE__ */ jsxDEV3(CrmPipelineBoard, {
830
+ dealsByStage,
831
+ stages
832
+ }, undefined, false, undefined, this);
833
+ }
834
+ var crmPipelineReactRenderer = {
835
+ target: "react",
836
+ render: async (desc, _ctx) => {
837
+ if (desc.source.type !== "component") {
838
+ throw new Error("Invalid source type");
839
+ }
840
+ if (desc.source.componentKey !== "CrmPipelineView") {
841
+ throw new Error(`Unknown component: ${desc.source.componentKey}`);
842
+ }
843
+ return /* @__PURE__ */ jsxDEV3(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
844
+ }
845
+ };
824
846
  export {
825
847
  crmPipelineReactRenderer,
826
848
  crmPipelineMarkdownRenderer,
@@ -23,6 +23,13 @@ function rowToDeal(row) {
23
23
  updatedAt: new Date(row.updatedAt)
24
24
  };
25
25
  }
26
+ var DEAL_SORT_COLUMNS = {
27
+ name: "name",
28
+ value: "value",
29
+ status: "status",
30
+ expectedCloseDate: "expectedCloseDate",
31
+ updatedAt: "updatedAt"
32
+ };
26
33
  function createCrmHandlers(db) {
27
34
  async function listDeals(input) {
28
35
  const {
@@ -33,7 +40,9 @@ function createCrmHandlers(db) {
33
40
  ownerId,
34
41
  search,
35
42
  limit = 20,
36
- offset = 0
43
+ offset = 0,
44
+ sortBy = "value",
45
+ sortDirection = "desc"
37
46
  } = input;
38
47
  let whereClause = "WHERE projectId = ?";
39
48
  const params = [projectId];
@@ -61,7 +70,9 @@ function createCrmHandlers(db) {
61
70
  const total = countResult[0]?.count ?? 0;
62
71
  const valueResult = (await db.query(`SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`, params)).rows;
63
72
  const totalValue = valueResult[0]?.total ?? 0;
64
- const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
73
+ const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
74
+ const orderByDirection = sortDirection === "asc" ? "ASC" : "DESC";
75
+ const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
65
76
  return {
66
77
  deals: dealRows.map(rowToDeal),
67
78
  total,
@@ -4,6 +4,6 @@
4
4
  * Renders the CRM pipeline board component.
5
5
  * Data is fetched via the CrmPipelineBoard component's internal hooks.
6
6
  */
7
- import * as React from 'react';
8
7
  import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
8
+ import * as React from 'react';
9
9
  export declare const crmPipelineReactRenderer: PresentationRenderer<React.ReactElement>;