@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.
- package/.turbo/turbo-build.log +8 -8
- package/AGENTS.md +51 -33
- package/README.md +66 -148
- package/dist/browser/events/contact.event.js +1 -1
- package/dist/browser/events/deal.event.js +1 -1
- package/dist/browser/events/index.js +3 -3
- package/dist/browser/events/task.event.js +1 -1
- package/dist/browser/index.js +293 -293
- package/dist/browser/ui/CrmDashboard.js +221 -221
- package/dist/browser/ui/CrmDealCard.js +5 -5
- package/dist/browser/ui/CrmPipelineBoard.js +13 -13
- package/dist/browser/ui/hooks/index.js +2 -2
- package/dist/browser/ui/hooks/useDealList.js +1 -1
- package/dist/browser/ui/hooks/useDealMutations.js +1 -1
- package/dist/browser/ui/index.js +290 -290
- package/dist/browser/ui/modals/CreateDealModal.js +12 -12
- package/dist/browser/ui/modals/DealActionsModal.js +21 -21
- package/dist/browser/ui/modals/index.js +33 -33
- package/dist/browser/ui/renderers/index.js +116 -116
- package/dist/browser/ui/renderers/pipeline.renderer.js +97 -97
- package/dist/deal/index.d.ts +2 -2
- package/dist/events/contact.event.js +1 -1
- package/dist/events/deal.event.js +1 -1
- package/dist/events/index.js +3 -3
- package/dist/events/task.event.js +1 -1
- package/dist/handlers/index.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +293 -293
- package/dist/node/events/contact.event.js +1 -1
- package/dist/node/events/deal.event.js +1 -1
- package/dist/node/events/index.js +3 -3
- package/dist/node/events/task.event.js +1 -1
- package/dist/node/index.js +293 -293
- package/dist/node/ui/CrmDashboard.js +221 -221
- package/dist/node/ui/CrmDealCard.js +5 -5
- package/dist/node/ui/CrmPipelineBoard.js +13 -13
- package/dist/node/ui/hooks/index.js +2 -2
- package/dist/node/ui/hooks/useDealList.js +1 -1
- package/dist/node/ui/hooks/useDealMutations.js +1 -1
- package/dist/node/ui/index.js +290 -290
- package/dist/node/ui/modals/CreateDealModal.js +12 -12
- package/dist/node/ui/modals/DealActionsModal.js +21 -21
- package/dist/node/ui/modals/index.js +33 -33
- package/dist/node/ui/renderers/index.js +116 -116
- package/dist/node/ui/renderers/pipeline.renderer.js +97 -97
- package/dist/operations/index.d.ts +1 -1
- package/dist/ui/CrmDashboard.js +221 -221
- package/dist/ui/CrmDealCard.js +5 -5
- package/dist/ui/CrmPipelineBoard.js +13 -13
- package/dist/ui/hooks/index.d.ts +2 -2
- package/dist/ui/hooks/index.js +2 -2
- package/dist/ui/hooks/useDealList.js +1 -1
- package/dist/ui/hooks/useDealMutations.d.ts +9 -0
- package/dist/ui/hooks/useDealMutations.js +1 -1
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +290 -290
- package/dist/ui/modals/CreateDealModal.js +12 -12
- package/dist/ui/modals/DealActionsModal.js +21 -21
- package/dist/ui/modals/index.js +33 -33
- package/dist/ui/renderers/index.d.ts +1 -1
- package/dist/ui/renderers/index.js +116 -116
- package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
- package/dist/ui/renderers/pipeline.renderer.js +97 -97
- package/package.json +10 -10
- package/src/crm-pipeline.feature.ts +86 -86
- package/src/deal/deal.enum.ts +8 -8
- package/src/deal/deal.operation.ts +255 -255
- package/src/deal/deal.schema.ts +92 -92
- package/src/deal/deal.test-spec.ts +48 -48
- package/src/deal/index.ts +17 -19
- package/src/docs/crm-pipeline.docblock.ts +43 -43
- package/src/entities/company.entity.ts +52 -52
- package/src/entities/contact.entity.ts +67 -67
- package/src/entities/deal.entity.ts +134 -134
- package/src/entities/index.ts +27 -27
- package/src/entities/task.entity.ts +105 -105
- package/src/events/contact.event.ts +22 -22
- package/src/events/deal.event.ts +77 -77
- package/src/events/task.event.ts +19 -19
- package/src/example.ts +32 -32
- package/src/handlers/crm.handlers.ts +358 -357
- package/src/handlers/deal.handlers.ts +179 -179
- package/src/handlers/index.ts +18 -19
- package/src/handlers/mock-data.ts +167 -167
- package/src/index.ts +11 -11
- package/src/operations/index.ts +16 -16
- package/src/presentations/dashboard.presentation.ts +45 -45
- package/src/presentations/pipeline.presentation.ts +90 -90
- package/src/seeders/index.ts +26 -26
- package/src/shared/overlay-types.ts +23 -23
- package/src/ui/CrmDashboard.tsx +256 -256
- package/src/ui/CrmDealCard.tsx +64 -64
- package/src/ui/CrmPipelineBoard.tsx +105 -105
- package/src/ui/hooks/index.ts +3 -3
- package/src/ui/hooks/useDealList.ts +85 -85
- package/src/ui/hooks/useDealMutations.ts +151 -150
- package/src/ui/index.ts +5 -10
- package/src/ui/modals/CreateDealModal.tsx +217 -217
- package/src/ui/modals/DealActionsModal.tsx +390 -390
- package/src/ui/overlays/demo-overlays.ts +43 -43
- package/src/ui/renderers/index.ts +4 -3
- package/src/ui/renderers/pipeline.markdown.ts +165 -165
- package/src/ui/renderers/pipeline.renderer.tsx +17 -16
- package/tsconfig.json +7 -8
- 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: "
|
|
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
|
|
468
|
+
className: "font-medium leading-snug",
|
|
551
469
|
children: deal.name
|
|
552
470
|
}, undefined, false, undefined, this),
|
|
553
471
|
/* @__PURE__ */ jsxDEV("div", {
|
|
554
|
-
className: "
|
|
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: "
|
|
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
|
|
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
|
|
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] =
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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/
|
|
685
|
-
import {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
|
26
|
+
className: "font-medium leading-snug",
|
|
109
27
|
children: deal.name
|
|
110
28
|
}, undefined, false, undefined, this),
|
|
111
29
|
/* @__PURE__ */ jsxDEV("div", {
|
|
112
|
-
className: "
|
|
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: "
|
|
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
|
|
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
|
|
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] =
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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.
|
|
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": "
|
|
327
|
-
"lint:check": "
|
|
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": "
|
|
335
|
-
"@contractspec/lib.design-system": "3.
|
|
336
|
-
"@contractspec/lib.example-shared-ui": "6.0.
|
|
337
|
-
"@contractspec/lib.identity-rbac": "3.7.
|
|
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.
|
|
341
|
-
"@contractspec/module.audit-trail": "3.7.
|
|
342
|
-
"@contractspec/module.notifications": "3.7.
|
|
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
|
},
|