@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
@@ -22,6 +22,13 @@ function rowToDeal(row) {
22
22
  updatedAt: new Date(row.updatedAt)
23
23
  };
24
24
  }
25
+ var DEAL_SORT_COLUMNS = {
26
+ name: "name",
27
+ value: "value",
28
+ status: "status",
29
+ expectedCloseDate: "expectedCloseDate",
30
+ updatedAt: "updatedAt"
31
+ };
25
32
  function createCrmHandlers(db) {
26
33
  async function listDeals(input) {
27
34
  const {
@@ -32,7 +39,9 @@ function createCrmHandlers(db) {
32
39
  ownerId,
33
40
  search,
34
41
  limit = 20,
35
- offset = 0
42
+ offset = 0,
43
+ sortBy = "value",
44
+ sortDirection = "desc"
36
45
  } = input;
37
46
  let whereClause = "WHERE projectId = ?";
38
47
  const params = [projectId];
@@ -60,7 +69,9 @@ function createCrmHandlers(db) {
60
69
  const total = countResult[0]?.count ?? 0;
61
70
  const valueResult = (await db.query(`SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`, params)).rows;
62
71
  const totalValue = valueResult[0]?.total ?? 0;
63
- const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
72
+ const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
73
+ const orderByDirection = sortDirection === "asc" ? "ASC" : "DESC";
74
+ const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
64
75
  return {
65
76
  deals: dealRows.map(rowToDeal),
66
77
  total,
@@ -440,88 +451,6 @@ async function mockGetDealsByStageHandler(input) {
440
451
  async function mockGetPipelineStagesHandler(input) {
441
452
  return MOCK_STAGES.filter((s) => s.pipelineId === input.pipelineId);
442
453
  }
443
- // src/ui/hooks/useDealList.ts
444
- import { useCallback, useEffect, useMemo, useState } from "react";
445
- import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
446
- "use client";
447
- function useDealList(options = {}) {
448
- const { handlers, projectId } = useTemplateRuntime();
449
- const { crm: crm2 } = handlers;
450
- const [data, setData] = useState(null);
451
- const [dealsByStage, setDealsByStage] = useState({});
452
- const [stages, setStages] = useState([]);
453
- const [loading, setLoading] = useState(true);
454
- const [error, setError] = useState(null);
455
- const [page, setPage] = useState(1);
456
- const pipelineId = options.pipelineId ?? "pipeline-1";
457
- const fetchData = useCallback(async () => {
458
- setLoading(true);
459
- setError(null);
460
- try {
461
- const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
462
- crm2.listDeals({
463
- projectId,
464
- pipelineId,
465
- stageId: options.stageId,
466
- status: options.status === "all" ? undefined : options.status,
467
- search: options.search,
468
- limit: options.limit ?? 50,
469
- offset: (page - 1) * (options.limit ?? 50)
470
- }),
471
- crm2.getDealsByStage({ projectId, pipelineId }),
472
- crm2.getPipelineStages({ pipelineId })
473
- ]);
474
- setData(dealsResult);
475
- setDealsByStage(stageDealsResult);
476
- setStages(stagesResult);
477
- } catch (err) {
478
- setError(err instanceof Error ? err : new Error("Unknown error"));
479
- } finally {
480
- setLoading(false);
481
- }
482
- }, [
483
- crm2,
484
- projectId,
485
- pipelineId,
486
- options.stageId,
487
- options.status,
488
- options.search,
489
- options.limit,
490
- page
491
- ]);
492
- useEffect(() => {
493
- fetchData();
494
- }, [fetchData]);
495
- const stats = useMemo(() => {
496
- if (!data)
497
- return null;
498
- const open = data.deals.filter((d) => d.status === "OPEN");
499
- const won = data.deals.filter((d) => d.status === "WON");
500
- const lost = data.deals.filter((d) => d.status === "LOST");
501
- return {
502
- total: data.total,
503
- totalValue: data.totalValue,
504
- openCount: open.length,
505
- openValue: open.reduce((sum, d) => sum + d.value, 0),
506
- wonCount: won.length,
507
- wonValue: won.reduce((sum, d) => sum + d.value, 0),
508
- lostCount: lost.length
509
- };
510
- }, [data]);
511
- return {
512
- data,
513
- dealsByStage,
514
- stages,
515
- loading,
516
- error,
517
- stats,
518
- page,
519
- refetch: fetchData,
520
- nextPage: () => setPage((p) => p + 1),
521
- prevPage: () => page > 1 && setPage((p) => p - 1)
522
- };
523
- }
524
-
525
454
  // src/ui/CrmDealCard.tsx
526
455
  import { jsxDEV } from "react/jsx-dev-runtime";
527
456
  "use client";
@@ -537,7 +466,7 @@ function CrmDealCard({ deal, onClick }) {
537
466
  const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
538
467
  return /* @__PURE__ */ jsxDEV("div", {
539
468
  onClick,
540
- className: "border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md",
469
+ className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
541
470
  role: "button",
542
471
  tabIndex: 0,
543
472
  onKeyDown: (e) => {
@@ -546,22 +475,22 @@ function CrmDealCard({ deal, onClick }) {
546
475
  },
547
476
  children: [
548
477
  /* @__PURE__ */ jsxDEV("h4", {
549
- className: "leading-snug font-medium",
478
+ className: "font-medium leading-snug",
550
479
  children: deal.name
551
480
  }, undefined, false, undefined, this),
552
481
  /* @__PURE__ */ jsxDEV("div", {
553
- className: "text-primary mt-2 text-lg font-semibold",
482
+ className: "mt-2 font-semibold text-lg text-primary",
554
483
  children: formatCurrency(deal.value, deal.currency)
555
484
  }, undefined, false, undefined, this),
556
485
  /* @__PURE__ */ jsxDEV("div", {
557
- className: "text-muted-foreground mt-3 flex items-center justify-between text-xs",
486
+ className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
558
487
  children: [
559
488
  daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
560
489
  className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
561
490
  children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
562
491
  }, undefined, false, undefined, this),
563
492
  /* @__PURE__ */ jsxDEV("span", {
564
- 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"}`,
493
+ 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"}`,
565
494
  children: deal.status
566
495
  }, undefined, false, undefined, this)
567
496
  ]
@@ -571,7 +500,7 @@ function CrmDealCard({ deal, onClick }) {
571
500
  }
572
501
 
573
502
  // src/ui/CrmPipelineBoard.tsx
574
- import { useState as useState2 } from "react";
503
+ import { useState } from "react";
575
504
  import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
576
505
  "use client";
577
506
  function formatCurrency2(value) {
@@ -587,7 +516,7 @@ function CrmPipelineBoard({
587
516
  onDealClick,
588
517
  onDealMove
589
518
  }) {
590
- const [quickMoveOpen, setQuickMoveOpen] = useState2(null);
519
+ const [quickMoveOpen, setQuickMoveOpen] = useState(null);
591
520
  const sortedStages = [...stages].sort((a, b) => a.position - b.position);
592
521
  const handleQuickMove = (dealId, toStageId) => {
593
522
  onDealMove?.(dealId, toStageId);
@@ -599,10 +528,10 @@ function CrmPipelineBoard({
599
528
  const deals = dealsByStage[stage.id] ?? [];
600
529
  const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
601
530
  return /* @__PURE__ */ jsxDEV2("div", {
602
- className: "bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg",
531
+ className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
603
532
  children: [
604
533
  /* @__PURE__ */ jsxDEV2("div", {
605
- className: "border-border flex items-center justify-between border-b px-3 py-2",
534
+ className: "flex items-center justify-between border-border border-b px-3 py-2",
606
535
  children: [
607
536
  /* @__PURE__ */ jsxDEV2("div", {
608
537
  children: [
@@ -621,7 +550,7 @@ function CrmPipelineBoard({
621
550
  ]
622
551
  }, undefined, true, undefined, this),
623
552
  /* @__PURE__ */ jsxDEV2("span", {
624
- className: "bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium",
553
+ className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
625
554
  children: deals.length
626
555
  }, undefined, false, undefined, this)
627
556
  ]
@@ -629,7 +558,7 @@ function CrmPipelineBoard({
629
558
  /* @__PURE__ */ jsxDEV2("div", {
630
559
  className: "flex flex-1 flex-col gap-2 p-2",
631
560
  children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
632
- className: "border-muted-foreground/20 text-muted-foreground flex h-24 items-center justify-center rounded-md border-2 border-dashed text-xs",
561
+ className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
633
562
  children: "No deals"
634
563
  }, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
635
564
  className: "group relative",
@@ -647,15 +576,15 @@ function CrmPipelineBoard({
647
576
  e.stopPropagation();
648
577
  setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
649
578
  },
650
- className: "bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm",
579
+ className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
651
580
  title: "Quick move",
652
581
  children: "➡️"
653
582
  }, undefined, false, undefined, this),
654
583
  quickMoveOpen === deal.id && /* @__PURE__ */ jsxDEV2("div", {
655
- className: "bg-card border-border absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border py-1 shadow-lg",
584
+ className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
656
585
  children: [
657
586
  /* @__PURE__ */ jsxDEV2("p", {
658
- className: "text-muted-foreground px-3 py-1 text-xs font-medium",
587
+ className: "px-3 py-1 font-medium text-muted-foreground text-xs",
659
588
  children: "Move to:"
660
589
  }, undefined, false, undefined, this),
661
590
  sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
@@ -664,7 +593,7 @@ function CrmPipelineBoard({
664
593
  e.stopPropagation();
665
594
  handleQuickMove(deal.id, s.id);
666
595
  },
667
- className: "hover:bg-muted w-full px-3 py-1.5 text-left text-sm",
596
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
668
597
  children: s.name
669
598
  }, s.id, false, undefined, this))
670
599
  ]
@@ -680,27 +609,98 @@ function CrmPipelineBoard({
680
609
  }, undefined, false, undefined, this);
681
610
  }
682
611
 
683
- // src/ui/renderers/pipeline.renderer.tsx
684
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
685
- function CrmPipelineBoardWrapper() {
686
- const { dealsByStage, stages } = useDealList();
687
- return /* @__PURE__ */ jsxDEV3(CrmPipelineBoard, {
612
+ // src/ui/hooks/useDealList.ts
613
+ import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
614
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
615
+ "use client";
616
+ function useDealList(options = {}) {
617
+ const { handlers, projectId } = useTemplateRuntime();
618
+ const { crm: crm2 } = handlers;
619
+ const [data, setData] = useState2(null);
620
+ const [dealsByStage, setDealsByStage] = useState2({});
621
+ const [stages, setStages] = useState2([]);
622
+ const [loading, setLoading] = useState2(true);
623
+ const [error, setError] = useState2(null);
624
+ const [internalPage, setInternalPage] = useState2(0);
625
+ const pipelineId = options.pipelineId ?? "pipeline-1";
626
+ const pageIndex = options.pageIndex ?? internalPage;
627
+ const pageSize = options.pageSize ?? options.limit ?? 50;
628
+ const [sort] = options.sorting ?? [];
629
+ const sortBy = sort?.id;
630
+ const sortDirection = sort ? sort.desc ? "desc" : "asc" : undefined;
631
+ const fetchData = useCallback(async () => {
632
+ setLoading(true);
633
+ setError(null);
634
+ try {
635
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
636
+ crm2.listDeals({
637
+ projectId,
638
+ pipelineId,
639
+ stageId: options.stageId,
640
+ status: options.status === "all" ? undefined : options.status,
641
+ search: options.search,
642
+ limit: pageSize,
643
+ offset: pageIndex * pageSize,
644
+ sortBy: sortBy === "name" || sortBy === "value" || sortBy === "status" || sortBy === "expectedCloseDate" || sortBy === "updatedAt" ? sortBy : undefined,
645
+ sortDirection
646
+ }),
647
+ crm2.getDealsByStage({ projectId, pipelineId }),
648
+ crm2.getPipelineStages({ pipelineId })
649
+ ]);
650
+ setData(dealsResult);
651
+ setDealsByStage(stageDealsResult);
652
+ setStages(stagesResult);
653
+ } catch (err) {
654
+ setError(err instanceof Error ? err : new Error("Unknown error"));
655
+ } finally {
656
+ setLoading(false);
657
+ }
658
+ }, [
659
+ crm2,
660
+ projectId,
661
+ pipelineId,
662
+ options.stageId,
663
+ options.status,
664
+ options.search,
665
+ pageIndex,
666
+ pageSize,
667
+ sortBy,
668
+ sortDirection
669
+ ]);
670
+ useEffect(() => {
671
+ fetchData();
672
+ }, [fetchData]);
673
+ const stats = useMemo(() => {
674
+ if (!data)
675
+ return null;
676
+ const open = data.deals.filter((d) => d.status === "OPEN");
677
+ const won = data.deals.filter((d) => d.status === "WON");
678
+ const lost = data.deals.filter((d) => d.status === "LOST");
679
+ return {
680
+ total: data.total,
681
+ totalValue: data.totalValue,
682
+ openCount: open.length,
683
+ openValue: open.reduce((sum, d) => sum + d.value, 0),
684
+ wonCount: won.length,
685
+ wonValue: won.reduce((sum, d) => sum + d.value, 0),
686
+ lostCount: lost.length
687
+ };
688
+ }, [data]);
689
+ return {
690
+ data,
688
691
  dealsByStage,
689
- stages
690
- }, undefined, false, undefined, this);
692
+ stages,
693
+ loading,
694
+ error,
695
+ stats,
696
+ page: pageIndex + 1,
697
+ pageIndex,
698
+ pageSize,
699
+ refetch: fetchData,
700
+ nextPage: options.pageIndex === undefined ? () => setInternalPage((page) => page + 1) : undefined,
701
+ prevPage: options.pageIndex === undefined ? () => pageIndex > 0 && setInternalPage((page) => page - 1) : undefined
702
+ };
691
703
  }
692
- var crmPipelineReactRenderer = {
693
- target: "react",
694
- render: async (desc, _ctx) => {
695
- if (desc.source.type !== "component") {
696
- throw new Error("Invalid source type");
697
- }
698
- if (desc.source.componentKey !== "CrmPipelineView") {
699
- throw new Error(`Unknown component: ${desc.source.componentKey}`);
700
- }
701
- return /* @__PURE__ */ jsxDEV3(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
702
- }
703
- };
704
704
 
705
705
  // src/ui/renderers/pipeline.markdown.ts
706
706
  function formatCurrency3(value, currency = "USD") {
@@ -820,6 +820,28 @@ var crmDashboardMarkdownRenderer = {
820
820
  };
821
821
  }
822
822
  };
823
+
824
+ // src/ui/renderers/pipeline.renderer.tsx
825
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
826
+ function CrmPipelineBoardWrapper() {
827
+ const { dealsByStage, stages } = useDealList();
828
+ return /* @__PURE__ */ jsxDEV3(CrmPipelineBoard, {
829
+ dealsByStage,
830
+ stages
831
+ }, undefined, false, undefined, this);
832
+ }
833
+ var crmPipelineReactRenderer = {
834
+ target: "react",
835
+ render: async (desc, _ctx) => {
836
+ if (desc.source.type !== "component") {
837
+ throw new Error("Invalid source type");
838
+ }
839
+ if (desc.source.componentKey !== "CrmPipelineView") {
840
+ throw new Error(`Unknown component: ${desc.source.componentKey}`);
841
+ }
842
+ return /* @__PURE__ */ jsxDEV3(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
843
+ }
844
+ };
823
845
  export {
824
846
  crmPipelineReactRenderer,
825
847
  crmPipelineMarkdownRenderer,
@@ -22,6 +22,13 @@ function rowToDeal(row) {
22
22
  updatedAt: new Date(row.updatedAt)
23
23
  };
24
24
  }
25
+ var DEAL_SORT_COLUMNS = {
26
+ name: "name",
27
+ value: "value",
28
+ status: "status",
29
+ expectedCloseDate: "expectedCloseDate",
30
+ updatedAt: "updatedAt"
31
+ };
25
32
  function createCrmHandlers(db) {
26
33
  async function listDeals(input) {
27
34
  const {
@@ -32,7 +39,9 @@ function createCrmHandlers(db) {
32
39
  ownerId,
33
40
  search,
34
41
  limit = 20,
35
- offset = 0
42
+ offset = 0,
43
+ sortBy = "value",
44
+ sortDirection = "desc"
36
45
  } = input;
37
46
  let whereClause = "WHERE projectId = ?";
38
47
  const params = [projectId];
@@ -60,7 +69,9 @@ function createCrmHandlers(db) {
60
69
  const total = countResult[0]?.count ?? 0;
61
70
  const valueResult = (await db.query(`SELECT COALESCE(SUM(value), 0) as total FROM crm_deal ${whereClause}`, params)).rows;
62
71
  const totalValue = valueResult[0]?.total ?? 0;
63
- const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
72
+ const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
73
+ const orderByDirection = sortDirection === "asc" ? "ASC" : "DESC";
74
+ const dealRows = (await db.query(`SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
64
75
  return {
65
76
  deals: dealRows.map(rowToDeal),
66
77
  total,