@contractspec/example.crm-pipeline 3.7.6 → 3.7.7

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 (105) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/AGENTS.md +51 -33
  3. package/README.md +66 -148
  4. package/dist/browser/events/contact.event.js +1 -1
  5. package/dist/browser/events/deal.event.js +1 -1
  6. package/dist/browser/events/index.js +3 -3
  7. package/dist/browser/events/task.event.js +1 -1
  8. package/dist/browser/index.js +293 -293
  9. package/dist/browser/ui/CrmDashboard.js +221 -221
  10. package/dist/browser/ui/CrmDealCard.js +5 -5
  11. package/dist/browser/ui/CrmPipelineBoard.js +13 -13
  12. package/dist/browser/ui/hooks/index.js +2 -2
  13. package/dist/browser/ui/hooks/useDealList.js +1 -1
  14. package/dist/browser/ui/hooks/useDealMutations.js +1 -1
  15. package/dist/browser/ui/index.js +290 -290
  16. package/dist/browser/ui/modals/CreateDealModal.js +12 -12
  17. package/dist/browser/ui/modals/DealActionsModal.js +21 -21
  18. package/dist/browser/ui/modals/index.js +33 -33
  19. package/dist/browser/ui/renderers/index.js +116 -116
  20. package/dist/browser/ui/renderers/pipeline.renderer.js +97 -97
  21. package/dist/deal/index.d.ts +2 -2
  22. package/dist/events/contact.event.js +1 -1
  23. package/dist/events/deal.event.js +1 -1
  24. package/dist/events/index.js +3 -3
  25. package/dist/events/task.event.js +1 -1
  26. package/dist/handlers/index.d.ts +2 -2
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.js +293 -293
  29. package/dist/node/events/contact.event.js +1 -1
  30. package/dist/node/events/deal.event.js +1 -1
  31. package/dist/node/events/index.js +3 -3
  32. package/dist/node/events/task.event.js +1 -1
  33. package/dist/node/index.js +293 -293
  34. package/dist/node/ui/CrmDashboard.js +221 -221
  35. package/dist/node/ui/CrmDealCard.js +5 -5
  36. package/dist/node/ui/CrmPipelineBoard.js +13 -13
  37. package/dist/node/ui/hooks/index.js +2 -2
  38. package/dist/node/ui/hooks/useDealList.js +1 -1
  39. package/dist/node/ui/hooks/useDealMutations.js +1 -1
  40. package/dist/node/ui/index.js +290 -290
  41. package/dist/node/ui/modals/CreateDealModal.js +12 -12
  42. package/dist/node/ui/modals/DealActionsModal.js +21 -21
  43. package/dist/node/ui/modals/index.js +33 -33
  44. package/dist/node/ui/renderers/index.js +116 -116
  45. package/dist/node/ui/renderers/pipeline.renderer.js +97 -97
  46. package/dist/operations/index.d.ts +1 -1
  47. package/dist/ui/CrmDashboard.js +221 -221
  48. package/dist/ui/CrmDealCard.js +5 -5
  49. package/dist/ui/CrmPipelineBoard.js +13 -13
  50. package/dist/ui/hooks/index.d.ts +2 -2
  51. package/dist/ui/hooks/index.js +2 -2
  52. package/dist/ui/hooks/useDealList.js +1 -1
  53. package/dist/ui/hooks/useDealMutations.d.ts +9 -0
  54. package/dist/ui/hooks/useDealMutations.js +1 -1
  55. package/dist/ui/index.d.ts +3 -3
  56. package/dist/ui/index.js +290 -290
  57. package/dist/ui/modals/CreateDealModal.js +12 -12
  58. package/dist/ui/modals/DealActionsModal.js +21 -21
  59. package/dist/ui/modals/index.js +33 -33
  60. package/dist/ui/renderers/index.d.ts +1 -1
  61. package/dist/ui/renderers/index.js +116 -116
  62. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  63. package/dist/ui/renderers/pipeline.renderer.js +97 -97
  64. package/package.json +10 -10
  65. package/src/crm-pipeline.feature.ts +86 -86
  66. package/src/deal/deal.enum.ts +8 -8
  67. package/src/deal/deal.operation.ts +255 -255
  68. package/src/deal/deal.schema.ts +92 -92
  69. package/src/deal/deal.test-spec.ts +48 -48
  70. package/src/deal/index.ts +17 -19
  71. package/src/docs/crm-pipeline.docblock.ts +43 -43
  72. package/src/entities/company.entity.ts +52 -52
  73. package/src/entities/contact.entity.ts +67 -67
  74. package/src/entities/deal.entity.ts +134 -134
  75. package/src/entities/index.ts +27 -27
  76. package/src/entities/task.entity.ts +105 -105
  77. package/src/events/contact.event.ts +22 -22
  78. package/src/events/deal.event.ts +77 -77
  79. package/src/events/task.event.ts +19 -19
  80. package/src/example.ts +32 -32
  81. package/src/handlers/crm.handlers.ts +358 -357
  82. package/src/handlers/deal.handlers.ts +179 -179
  83. package/src/handlers/index.ts +18 -19
  84. package/src/handlers/mock-data.ts +167 -167
  85. package/src/index.ts +11 -11
  86. package/src/operations/index.ts +16 -16
  87. package/src/presentations/dashboard.presentation.ts +45 -45
  88. package/src/presentations/pipeline.presentation.ts +90 -90
  89. package/src/seeders/index.ts +26 -26
  90. package/src/shared/overlay-types.ts +23 -23
  91. package/src/ui/CrmDashboard.tsx +256 -256
  92. package/src/ui/CrmDealCard.tsx +64 -64
  93. package/src/ui/CrmPipelineBoard.tsx +105 -105
  94. package/src/ui/hooks/index.ts +3 -3
  95. package/src/ui/hooks/useDealList.ts +85 -85
  96. package/src/ui/hooks/useDealMutations.ts +151 -150
  97. package/src/ui/index.ts +5 -10
  98. package/src/ui/modals/CreateDealModal.tsx +217 -217
  99. package/src/ui/modals/DealActionsModal.tsx +390 -390
  100. package/src/ui/overlays/demo-overlays.ts +43 -43
  101. package/src/ui/renderers/index.ts +4 -3
  102. package/src/ui/renderers/pipeline.markdown.ts +165 -165
  103. package/src/ui/renderers/pipeline.renderer.tsx +17 -16
  104. package/tsconfig.json +7 -8
  105. package/tsdown.config.js +7 -3
@@ -441,88 +441,6 @@ async function mockGetDealsByStageHandler(input) {
441
441
  async function mockGetPipelineStagesHandler(input) {
442
442
  return MOCK_STAGES.filter((s) => s.pipelineId === input.pipelineId);
443
443
  }
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
444
  // src/ui/CrmDealCard.tsx
527
445
  import { jsxDEV } from "react/jsx-dev-runtime";
528
446
  "use client";
@@ -538,7 +456,7 @@ function CrmDealCard({ deal, onClick }) {
538
456
  const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
539
457
  return /* @__PURE__ */ jsxDEV("div", {
540
458
  onClick,
541
- className: "border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md",
459
+ className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
542
460
  role: "button",
543
461
  tabIndex: 0,
544
462
  onKeyDown: (e) => {
@@ -547,22 +465,22 @@ function CrmDealCard({ deal, onClick }) {
547
465
  },
548
466
  children: [
549
467
  /* @__PURE__ */ jsxDEV("h4", {
550
- className: "leading-snug font-medium",
468
+ className: "font-medium leading-snug",
551
469
  children: deal.name
552
470
  }, undefined, false, undefined, this),
553
471
  /* @__PURE__ */ jsxDEV("div", {
554
- className: "text-primary mt-2 text-lg font-semibold",
472
+ className: "mt-2 font-semibold text-lg text-primary",
555
473
  children: formatCurrency(deal.value, deal.currency)
556
474
  }, undefined, false, undefined, this),
557
475
  /* @__PURE__ */ jsxDEV("div", {
558
- className: "text-muted-foreground mt-3 flex items-center justify-between text-xs",
476
+ className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
559
477
  children: [
560
478
  daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
561
479
  className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
562
480
  children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
563
481
  }, undefined, false, undefined, this),
564
482
  /* @__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"}`,
483
+ 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
484
  children: deal.status
567
485
  }, undefined, false, undefined, this)
568
486
  ]
@@ -572,7 +490,7 @@ function CrmDealCard({ deal, onClick }) {
572
490
  }
573
491
 
574
492
  // src/ui/CrmPipelineBoard.tsx
575
- import { useState as useState2 } from "react";
493
+ import { useState } from "react";
576
494
  import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
577
495
  "use client";
578
496
  function formatCurrency2(value) {
@@ -588,7 +506,7 @@ function CrmPipelineBoard({
588
506
  onDealClick,
589
507
  onDealMove
590
508
  }) {
591
- const [quickMoveOpen, setQuickMoveOpen] = useState2(null);
509
+ const [quickMoveOpen, setQuickMoveOpen] = useState(null);
592
510
  const sortedStages = [...stages].sort((a, b) => a.position - b.position);
593
511
  const handleQuickMove = (dealId, toStageId) => {
594
512
  onDealMove?.(dealId, toStageId);
@@ -600,10 +518,10 @@ function CrmPipelineBoard({
600
518
  const deals = dealsByStage[stage.id] ?? [];
601
519
  const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
602
520
  return /* @__PURE__ */ jsxDEV2("div", {
603
- className: "bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg",
521
+ className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
604
522
  children: [
605
523
  /* @__PURE__ */ jsxDEV2("div", {
606
- className: "border-border flex items-center justify-between border-b px-3 py-2",
524
+ className: "flex items-center justify-between border-border border-b px-3 py-2",
607
525
  children: [
608
526
  /* @__PURE__ */ jsxDEV2("div", {
609
527
  children: [
@@ -622,7 +540,7 @@ function CrmPipelineBoard({
622
540
  ]
623
541
  }, undefined, true, undefined, this),
624
542
  /* @__PURE__ */ jsxDEV2("span", {
625
- className: "bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium",
543
+ className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
626
544
  children: deals.length
627
545
  }, undefined, false, undefined, this)
628
546
  ]
@@ -630,7 +548,7 @@ function CrmPipelineBoard({
630
548
  /* @__PURE__ */ jsxDEV2("div", {
631
549
  className: "flex flex-1 flex-col gap-2 p-2",
632
550
  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",
551
+ className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
634
552
  children: "No deals"
635
553
  }, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
636
554
  className: "group relative",
@@ -648,15 +566,15 @@ function CrmPipelineBoard({
648
566
  e.stopPropagation();
649
567
  setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
650
568
  },
651
- className: "bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm",
569
+ className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
652
570
  title: "Quick move",
653
571
  children: "\u27A1\uFE0F"
654
572
  }, undefined, false, undefined, this),
655
573
  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",
574
+ className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
657
575
  children: [
658
576
  /* @__PURE__ */ jsxDEV2("p", {
659
- className: "text-muted-foreground px-3 py-1 text-xs font-medium",
577
+ className: "px-3 py-1 font-medium text-muted-foreground text-xs",
660
578
  children: "Move to:"
661
579
  }, undefined, false, undefined, this),
662
580
  sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
@@ -665,7 +583,7 @@ function CrmPipelineBoard({
665
583
  e.stopPropagation();
666
584
  handleQuickMove(deal.id, s.id);
667
585
  },
668
- className: "hover:bg-muted w-full px-3 py-1.5 text-left text-sm",
586
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
669
587
  children: s.name
670
588
  }, s.id, false, undefined, this))
671
589
  ]
@@ -681,27 +599,87 @@ function CrmPipelineBoard({
681
599
  }, undefined, false, undefined, this);
682
600
  }
683
601
 
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, {
602
+ // src/ui/hooks/useDealList.ts
603
+ import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
604
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
605
+ "use client";
606
+ function useDealList(options = {}) {
607
+ const { handlers, projectId } = useTemplateRuntime();
608
+ const { crm: crm2 } = handlers;
609
+ const [data, setData] = useState2(null);
610
+ const [dealsByStage, setDealsByStage] = useState2({});
611
+ const [stages, setStages] = useState2([]);
612
+ const [loading, setLoading] = useState2(true);
613
+ const [error, setError] = useState2(null);
614
+ const [page, setPage] = useState2(1);
615
+ const pipelineId = options.pipelineId ?? "pipeline-1";
616
+ const fetchData = useCallback(async () => {
617
+ setLoading(true);
618
+ setError(null);
619
+ try {
620
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
621
+ crm2.listDeals({
622
+ projectId,
623
+ pipelineId,
624
+ stageId: options.stageId,
625
+ status: options.status === "all" ? undefined : options.status,
626
+ search: options.search,
627
+ limit: options.limit ?? 50,
628
+ offset: (page - 1) * (options.limit ?? 50)
629
+ }),
630
+ crm2.getDealsByStage({ projectId, pipelineId }),
631
+ crm2.getPipelineStages({ pipelineId })
632
+ ]);
633
+ setData(dealsResult);
634
+ setDealsByStage(stageDealsResult);
635
+ setStages(stagesResult);
636
+ } catch (err) {
637
+ setError(err instanceof Error ? err : new Error("Unknown error"));
638
+ } finally {
639
+ setLoading(false);
640
+ }
641
+ }, [
642
+ crm2,
643
+ projectId,
644
+ pipelineId,
645
+ options.stageId,
646
+ options.status,
647
+ options.search,
648
+ options.limit,
649
+ page
650
+ ]);
651
+ useEffect(() => {
652
+ fetchData();
653
+ }, [fetchData]);
654
+ const stats = useMemo(() => {
655
+ if (!data)
656
+ return null;
657
+ const open = data.deals.filter((d) => d.status === "OPEN");
658
+ const won = data.deals.filter((d) => d.status === "WON");
659
+ const lost = data.deals.filter((d) => d.status === "LOST");
660
+ return {
661
+ total: data.total,
662
+ totalValue: data.totalValue,
663
+ openCount: open.length,
664
+ openValue: open.reduce((sum, d) => sum + d.value, 0),
665
+ wonCount: won.length,
666
+ wonValue: won.reduce((sum, d) => sum + d.value, 0),
667
+ lostCount: lost.length
668
+ };
669
+ }, [data]);
670
+ return {
671
+ data,
689
672
  dealsByStage,
690
- stages
691
- }, undefined, false, undefined, this);
673
+ stages,
674
+ loading,
675
+ error,
676
+ stats,
677
+ page,
678
+ refetch: fetchData,
679
+ nextPage: () => setPage((p) => p + 1),
680
+ prevPage: () => page > 1 && setPage((p) => p - 1)
681
+ };
692
682
  }
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
683
 
706
684
  // src/ui/renderers/pipeline.markdown.ts
707
685
  function formatCurrency3(value, currency = "USD") {
@@ -821,6 +799,28 @@ var crmDashboardMarkdownRenderer = {
821
799
  };
822
800
  }
823
801
  };
802
+
803
+ // src/ui/renderers/pipeline.renderer.tsx
804
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
805
+ function CrmPipelineBoardWrapper() {
806
+ const { dealsByStage, stages } = useDealList();
807
+ return /* @__PURE__ */ jsxDEV3(CrmPipelineBoard, {
808
+ dealsByStage,
809
+ stages
810
+ }, undefined, false, undefined, this);
811
+ }
812
+ var crmPipelineReactRenderer = {
813
+ target: "react",
814
+ render: async (desc, _ctx) => {
815
+ if (desc.source.type !== "component") {
816
+ throw new Error("Invalid source type");
817
+ }
818
+ if (desc.source.componentKey !== "CrmPipelineView") {
819
+ throw new Error(`Unknown component: ${desc.source.componentKey}`);
820
+ }
821
+ return /* @__PURE__ */ jsxDEV3(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
822
+ }
823
+ };
824
824
  export {
825
825
  crmPipelineReactRenderer,
826
826
  crmPipelineMarkdownRenderer,
@@ -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>;
@@ -1,86 +1,4 @@
1
1
  // @bun
2
- // src/ui/hooks/useDealList.ts
3
- import { useCallback, useEffect, useMemo, useState } from "react";
4
- import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
5
- "use client";
6
- function useDealList(options = {}) {
7
- const { handlers, projectId } = useTemplateRuntime();
8
- const { crm } = handlers;
9
- const [data, setData] = useState(null);
10
- const [dealsByStage, setDealsByStage] = useState({});
11
- const [stages, setStages] = useState([]);
12
- const [loading, setLoading] = useState(true);
13
- const [error, setError] = useState(null);
14
- const [page, setPage] = useState(1);
15
- const pipelineId = options.pipelineId ?? "pipeline-1";
16
- const fetchData = useCallback(async () => {
17
- setLoading(true);
18
- setError(null);
19
- try {
20
- const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
21
- crm.listDeals({
22
- projectId,
23
- pipelineId,
24
- stageId: options.stageId,
25
- status: options.status === "all" ? undefined : options.status,
26
- search: options.search,
27
- limit: options.limit ?? 50,
28
- offset: (page - 1) * (options.limit ?? 50)
29
- }),
30
- crm.getDealsByStage({ projectId, pipelineId }),
31
- crm.getPipelineStages({ pipelineId })
32
- ]);
33
- setData(dealsResult);
34
- setDealsByStage(stageDealsResult);
35
- setStages(stagesResult);
36
- } catch (err) {
37
- setError(err instanceof Error ? err : new Error("Unknown error"));
38
- } finally {
39
- setLoading(false);
40
- }
41
- }, [
42
- crm,
43
- projectId,
44
- pipelineId,
45
- options.stageId,
46
- options.status,
47
- options.search,
48
- options.limit,
49
- page
50
- ]);
51
- useEffect(() => {
52
- fetchData();
53
- }, [fetchData]);
54
- const stats = useMemo(() => {
55
- if (!data)
56
- return null;
57
- const open = data.deals.filter((d) => d.status === "OPEN");
58
- const won = data.deals.filter((d) => d.status === "WON");
59
- const lost = data.deals.filter((d) => d.status === "LOST");
60
- return {
61
- total: data.total,
62
- totalValue: data.totalValue,
63
- openCount: open.length,
64
- openValue: open.reduce((sum, d) => sum + d.value, 0),
65
- wonCount: won.length,
66
- wonValue: won.reduce((sum, d) => sum + d.value, 0),
67
- lostCount: lost.length
68
- };
69
- }, [data]);
70
- return {
71
- data,
72
- dealsByStage,
73
- stages,
74
- loading,
75
- error,
76
- stats,
77
- page,
78
- refetch: fetchData,
79
- nextPage: () => setPage((p) => p + 1),
80
- prevPage: () => page > 1 && setPage((p) => p - 1)
81
- };
82
- }
83
-
84
2
  // src/ui/CrmDealCard.tsx
85
3
  import { jsxDEV } from "react/jsx-dev-runtime";
86
4
  "use client";
@@ -96,7 +14,7 @@ function CrmDealCard({ deal, onClick }) {
96
14
  const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
97
15
  return /* @__PURE__ */ jsxDEV("div", {
98
16
  onClick,
99
- className: "border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md",
17
+ className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
100
18
  role: "button",
101
19
  tabIndex: 0,
102
20
  onKeyDown: (e) => {
@@ -105,22 +23,22 @@ function CrmDealCard({ deal, onClick }) {
105
23
  },
106
24
  children: [
107
25
  /* @__PURE__ */ jsxDEV("h4", {
108
- className: "leading-snug font-medium",
26
+ className: "font-medium leading-snug",
109
27
  children: deal.name
110
28
  }, undefined, false, undefined, this),
111
29
  /* @__PURE__ */ jsxDEV("div", {
112
- className: "text-primary mt-2 text-lg font-semibold",
30
+ className: "mt-2 font-semibold text-lg text-primary",
113
31
  children: formatCurrency(deal.value, deal.currency)
114
32
  }, undefined, false, undefined, this),
115
33
  /* @__PURE__ */ jsxDEV("div", {
116
- className: "text-muted-foreground mt-3 flex items-center justify-between text-xs",
34
+ className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
117
35
  children: [
118
36
  daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
119
37
  className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
120
38
  children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
121
39
  }, undefined, false, undefined, this),
122
40
  /* @__PURE__ */ jsxDEV("span", {
123
- 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"}`,
41
+ 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"}`,
124
42
  children: deal.status
125
43
  }, undefined, false, undefined, this)
126
44
  ]
@@ -130,7 +48,7 @@ function CrmDealCard({ deal, onClick }) {
130
48
  }
131
49
 
132
50
  // src/ui/CrmPipelineBoard.tsx
133
- import { useState as useState2 } from "react";
51
+ import { useState } from "react";
134
52
  import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
135
53
  "use client";
136
54
  function formatCurrency2(value) {
@@ -146,7 +64,7 @@ function CrmPipelineBoard({
146
64
  onDealClick,
147
65
  onDealMove
148
66
  }) {
149
- const [quickMoveOpen, setQuickMoveOpen] = useState2(null);
67
+ const [quickMoveOpen, setQuickMoveOpen] = useState(null);
150
68
  const sortedStages = [...stages].sort((a, b) => a.position - b.position);
151
69
  const handleQuickMove = (dealId, toStageId) => {
152
70
  onDealMove?.(dealId, toStageId);
@@ -158,10 +76,10 @@ function CrmPipelineBoard({
158
76
  const deals = dealsByStage[stage.id] ?? [];
159
77
  const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
160
78
  return /* @__PURE__ */ jsxDEV2("div", {
161
- className: "bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg",
79
+ className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
162
80
  children: [
163
81
  /* @__PURE__ */ jsxDEV2("div", {
164
- className: "border-border flex items-center justify-between border-b px-3 py-2",
82
+ className: "flex items-center justify-between border-border border-b px-3 py-2",
165
83
  children: [
166
84
  /* @__PURE__ */ jsxDEV2("div", {
167
85
  children: [
@@ -180,7 +98,7 @@ function CrmPipelineBoard({
180
98
  ]
181
99
  }, undefined, true, undefined, this),
182
100
  /* @__PURE__ */ jsxDEV2("span", {
183
- className: "bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium",
101
+ className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
184
102
  children: deals.length
185
103
  }, undefined, false, undefined, this)
186
104
  ]
@@ -188,7 +106,7 @@ function CrmPipelineBoard({
188
106
  /* @__PURE__ */ jsxDEV2("div", {
189
107
  className: "flex flex-1 flex-col gap-2 p-2",
190
108
  children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
191
- className: "border-muted-foreground/20 text-muted-foreground flex h-24 items-center justify-center rounded-md border-2 border-dashed text-xs",
109
+ className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
192
110
  children: "No deals"
193
111
  }, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
194
112
  className: "group relative",
@@ -206,15 +124,15 @@ function CrmPipelineBoard({
206
124
  e.stopPropagation();
207
125
  setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
208
126
  },
209
- className: "bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm",
127
+ className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
210
128
  title: "Quick move",
211
129
  children: "\u27A1\uFE0F"
212
130
  }, undefined, false, undefined, this),
213
131
  quickMoveOpen === deal.id && /* @__PURE__ */ jsxDEV2("div", {
214
- className: "bg-card border-border absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border py-1 shadow-lg",
132
+ className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
215
133
  children: [
216
134
  /* @__PURE__ */ jsxDEV2("p", {
217
- className: "text-muted-foreground px-3 py-1 text-xs font-medium",
135
+ className: "px-3 py-1 font-medium text-muted-foreground text-xs",
218
136
  children: "Move to:"
219
137
  }, undefined, false, undefined, this),
220
138
  sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
@@ -223,7 +141,7 @@ function CrmPipelineBoard({
223
141
  e.stopPropagation();
224
142
  handleQuickMove(deal.id, s.id);
225
143
  },
226
- className: "hover:bg-muted w-full px-3 py-1.5 text-left text-sm",
144
+ className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
227
145
  children: s.name
228
146
  }, s.id, false, undefined, this))
229
147
  ]
@@ -239,6 +157,88 @@ function CrmPipelineBoard({
239
157
  }, undefined, false, undefined, this);
240
158
  }
241
159
 
160
+ // src/ui/hooks/useDealList.ts
161
+ import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
162
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
163
+ "use client";
164
+ function useDealList(options = {}) {
165
+ const { handlers, projectId } = useTemplateRuntime();
166
+ const { crm } = handlers;
167
+ const [data, setData] = useState2(null);
168
+ const [dealsByStage, setDealsByStage] = useState2({});
169
+ const [stages, setStages] = useState2([]);
170
+ const [loading, setLoading] = useState2(true);
171
+ const [error, setError] = useState2(null);
172
+ const [page, setPage] = useState2(1);
173
+ const pipelineId = options.pipelineId ?? "pipeline-1";
174
+ const fetchData = useCallback(async () => {
175
+ setLoading(true);
176
+ setError(null);
177
+ try {
178
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
179
+ crm.listDeals({
180
+ projectId,
181
+ pipelineId,
182
+ stageId: options.stageId,
183
+ status: options.status === "all" ? undefined : options.status,
184
+ search: options.search,
185
+ limit: options.limit ?? 50,
186
+ offset: (page - 1) * (options.limit ?? 50)
187
+ }),
188
+ crm.getDealsByStage({ projectId, pipelineId }),
189
+ crm.getPipelineStages({ pipelineId })
190
+ ]);
191
+ setData(dealsResult);
192
+ setDealsByStage(stageDealsResult);
193
+ setStages(stagesResult);
194
+ } catch (err) {
195
+ setError(err instanceof Error ? err : new Error("Unknown error"));
196
+ } finally {
197
+ setLoading(false);
198
+ }
199
+ }, [
200
+ crm,
201
+ projectId,
202
+ pipelineId,
203
+ options.stageId,
204
+ options.status,
205
+ options.search,
206
+ options.limit,
207
+ page
208
+ ]);
209
+ useEffect(() => {
210
+ fetchData();
211
+ }, [fetchData]);
212
+ const stats = useMemo(() => {
213
+ if (!data)
214
+ return null;
215
+ const open = data.deals.filter((d) => d.status === "OPEN");
216
+ const won = data.deals.filter((d) => d.status === "WON");
217
+ const lost = data.deals.filter((d) => d.status === "LOST");
218
+ return {
219
+ total: data.total,
220
+ totalValue: data.totalValue,
221
+ openCount: open.length,
222
+ openValue: open.reduce((sum, d) => sum + d.value, 0),
223
+ wonCount: won.length,
224
+ wonValue: won.reduce((sum, d) => sum + d.value, 0),
225
+ lostCount: lost.length
226
+ };
227
+ }, [data]);
228
+ return {
229
+ data,
230
+ dealsByStage,
231
+ stages,
232
+ loading,
233
+ error,
234
+ stats,
235
+ page,
236
+ refetch: fetchData,
237
+ nextPage: () => setPage((p) => p + 1),
238
+ prevPage: () => page > 1 && setPage((p) => p - 1)
239
+ };
240
+ }
241
+
242
242
  // src/ui/renderers/pipeline.renderer.tsx
243
243
  import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
244
244
  function CrmPipelineBoardWrapper() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractspec/example.crm-pipeline",
3
- "version": "3.7.6",
3
+ "version": "3.7.7",
4
4
  "description": "CRM Pipeline - Contacts, Companies, Deals, Tasks",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -323,23 +323,23 @@
323
323
  "dev": "contractspec-bun-build dev",
324
324
  "clean": "rimraf dist .turbo",
325
325
  "lint": "bun lint:fix",
326
- "lint:fix": "eslint src --fix",
327
- "lint:check": "eslint src",
326
+ "lint:fix": "biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .",
327
+ "lint:check": "biome check .",
328
328
  "test": "bun test --pass-with-no-tests",
329
329
  "validate": "contractspec validate \"src/**/*\"",
330
330
  "prebuild": "contractspec-bun-build prebuild",
331
331
  "typecheck": "tsc --noEmit"
332
332
  },
333
333
  "dependencies": {
334
- "@contractspec/lib.contracts-spec": "3.7.6",
335
- "@contractspec/lib.design-system": "3.7.6",
336
- "@contractspec/lib.example-shared-ui": "6.0.6",
337
- "@contractspec/lib.identity-rbac": "3.7.6",
334
+ "@contractspec/lib.contracts-spec": "4.0.0",
335
+ "@contractspec/lib.design-system": "3.8.0",
336
+ "@contractspec/lib.example-shared-ui": "6.0.7",
337
+ "@contractspec/lib.identity-rbac": "3.7.7",
338
338
  "@contractspec/lib.runtime-sandbox": "2.7.6",
339
339
  "@contractspec/lib.schema": "3.7.6",
340
- "@contractspec/lib.ui-kit-web": "3.7.6",
341
- "@contractspec/module.audit-trail": "3.7.6",
342
- "@contractspec/module.notifications": "3.7.6",
340
+ "@contractspec/lib.ui-kit-web": "3.8.0",
341
+ "@contractspec/module.audit-trail": "3.7.7",
342
+ "@contractspec/module.notifications": "3.7.7",
343
343
  "react": "19.2.0",
344
344
  "react-dom": "19.2.0"
345
345
  },