@contractspec/example.crm-pipeline 3.7.5 → 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/CHANGELOG.md +16 -0
- 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 +14 -14
- 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
|
@@ -440,88 +440,6 @@ async function mockGetDealsByStageHandler(input) {
|
|
|
440
440
|
async function mockGetPipelineStagesHandler(input) {
|
|
441
441
|
return MOCK_STAGES.filter((s) => s.pipelineId === input.pipelineId);
|
|
442
442
|
}
|
|
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
443
|
// src/ui/CrmDealCard.tsx
|
|
526
444
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
527
445
|
"use client";
|
|
@@ -537,7 +455,7 @@ function CrmDealCard({ deal, onClick }) {
|
|
|
537
455
|
const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
|
|
538
456
|
return /* @__PURE__ */ jsxDEV("div", {
|
|
539
457
|
onClick,
|
|
540
|
-
className: "
|
|
458
|
+
className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
|
|
541
459
|
role: "button",
|
|
542
460
|
tabIndex: 0,
|
|
543
461
|
onKeyDown: (e) => {
|
|
@@ -546,22 +464,22 @@ function CrmDealCard({ deal, onClick }) {
|
|
|
546
464
|
},
|
|
547
465
|
children: [
|
|
548
466
|
/* @__PURE__ */ jsxDEV("h4", {
|
|
549
|
-
className: "leading-snug
|
|
467
|
+
className: "font-medium leading-snug",
|
|
550
468
|
children: deal.name
|
|
551
469
|
}, undefined, false, undefined, this),
|
|
552
470
|
/* @__PURE__ */ jsxDEV("div", {
|
|
553
|
-
className: "
|
|
471
|
+
className: "mt-2 font-semibold text-lg text-primary",
|
|
554
472
|
children: formatCurrency(deal.value, deal.currency)
|
|
555
473
|
}, undefined, false, undefined, this),
|
|
556
474
|
/* @__PURE__ */ jsxDEV("div", {
|
|
557
|
-
className: "
|
|
475
|
+
className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
|
|
558
476
|
children: [
|
|
559
477
|
daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
|
|
560
478
|
className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
|
|
561
479
|
children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
|
|
562
480
|
}, undefined, false, undefined, this),
|
|
563
481
|
/* @__PURE__ */ jsxDEV("span", {
|
|
564
|
-
className: `rounded px-1.5 py-0.5 text-xs
|
|
482
|
+
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
483
|
children: deal.status
|
|
566
484
|
}, undefined, false, undefined, this)
|
|
567
485
|
]
|
|
@@ -571,7 +489,7 @@ function CrmDealCard({ deal, onClick }) {
|
|
|
571
489
|
}
|
|
572
490
|
|
|
573
491
|
// src/ui/CrmPipelineBoard.tsx
|
|
574
|
-
import { useState
|
|
492
|
+
import { useState } from "react";
|
|
575
493
|
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
576
494
|
"use client";
|
|
577
495
|
function formatCurrency2(value) {
|
|
@@ -587,7 +505,7 @@ function CrmPipelineBoard({
|
|
|
587
505
|
onDealClick,
|
|
588
506
|
onDealMove
|
|
589
507
|
}) {
|
|
590
|
-
const [quickMoveOpen, setQuickMoveOpen] =
|
|
508
|
+
const [quickMoveOpen, setQuickMoveOpen] = useState(null);
|
|
591
509
|
const sortedStages = [...stages].sort((a, b) => a.position - b.position);
|
|
592
510
|
const handleQuickMove = (dealId, toStageId) => {
|
|
593
511
|
onDealMove?.(dealId, toStageId);
|
|
@@ -599,10 +517,10 @@ function CrmPipelineBoard({
|
|
|
599
517
|
const deals = dealsByStage[stage.id] ?? [];
|
|
600
518
|
const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
|
|
601
519
|
return /* @__PURE__ */ jsxDEV2("div", {
|
|
602
|
-
className: "
|
|
520
|
+
className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
|
|
603
521
|
children: [
|
|
604
522
|
/* @__PURE__ */ jsxDEV2("div", {
|
|
605
|
-
className: "
|
|
523
|
+
className: "flex items-center justify-between border-border border-b px-3 py-2",
|
|
606
524
|
children: [
|
|
607
525
|
/* @__PURE__ */ jsxDEV2("div", {
|
|
608
526
|
children: [
|
|
@@ -621,7 +539,7 @@ function CrmPipelineBoard({
|
|
|
621
539
|
]
|
|
622
540
|
}, undefined, true, undefined, this),
|
|
623
541
|
/* @__PURE__ */ jsxDEV2("span", {
|
|
624
|
-
className: "
|
|
542
|
+
className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
|
|
625
543
|
children: deals.length
|
|
626
544
|
}, undefined, false, undefined, this)
|
|
627
545
|
]
|
|
@@ -629,7 +547,7 @@ function CrmPipelineBoard({
|
|
|
629
547
|
/* @__PURE__ */ jsxDEV2("div", {
|
|
630
548
|
className: "flex flex-1 flex-col gap-2 p-2",
|
|
631
549
|
children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
|
|
632
|
-
className: "
|
|
550
|
+
className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
|
|
633
551
|
children: "No deals"
|
|
634
552
|
}, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
|
|
635
553
|
className: "group relative",
|
|
@@ -647,15 +565,15 @@ function CrmPipelineBoard({
|
|
|
647
565
|
e.stopPropagation();
|
|
648
566
|
setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
|
|
649
567
|
},
|
|
650
|
-
className: "
|
|
568
|
+
className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
|
|
651
569
|
title: "Quick move",
|
|
652
570
|
children: "➡️"
|
|
653
571
|
}, undefined, false, undefined, this),
|
|
654
572
|
quickMoveOpen === deal.id && /* @__PURE__ */ jsxDEV2("div", {
|
|
655
|
-
className: "
|
|
573
|
+
className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
|
|
656
574
|
children: [
|
|
657
575
|
/* @__PURE__ */ jsxDEV2("p", {
|
|
658
|
-
className: "
|
|
576
|
+
className: "px-3 py-1 font-medium text-muted-foreground text-xs",
|
|
659
577
|
children: "Move to:"
|
|
660
578
|
}, undefined, false, undefined, this),
|
|
661
579
|
sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
|
|
@@ -664,7 +582,7 @@ function CrmPipelineBoard({
|
|
|
664
582
|
e.stopPropagation();
|
|
665
583
|
handleQuickMove(deal.id, s.id);
|
|
666
584
|
},
|
|
667
|
-
className: "
|
|
585
|
+
className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
|
|
668
586
|
children: s.name
|
|
669
587
|
}, s.id, false, undefined, this))
|
|
670
588
|
]
|
|
@@ -680,27 +598,87 @@ function CrmPipelineBoard({
|
|
|
680
598
|
}, undefined, false, undefined, this);
|
|
681
599
|
}
|
|
682
600
|
|
|
683
|
-
// src/ui/
|
|
684
|
-
import {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
601
|
+
// src/ui/hooks/useDealList.ts
|
|
602
|
+
import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
|
|
603
|
+
import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
|
|
604
|
+
"use client";
|
|
605
|
+
function useDealList(options = {}) {
|
|
606
|
+
const { handlers, projectId } = useTemplateRuntime();
|
|
607
|
+
const { crm: crm2 } = handlers;
|
|
608
|
+
const [data, setData] = useState2(null);
|
|
609
|
+
const [dealsByStage, setDealsByStage] = useState2({});
|
|
610
|
+
const [stages, setStages] = useState2([]);
|
|
611
|
+
const [loading, setLoading] = useState2(true);
|
|
612
|
+
const [error, setError] = useState2(null);
|
|
613
|
+
const [page, setPage] = useState2(1);
|
|
614
|
+
const pipelineId = options.pipelineId ?? "pipeline-1";
|
|
615
|
+
const fetchData = useCallback(async () => {
|
|
616
|
+
setLoading(true);
|
|
617
|
+
setError(null);
|
|
618
|
+
try {
|
|
619
|
+
const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
|
|
620
|
+
crm2.listDeals({
|
|
621
|
+
projectId,
|
|
622
|
+
pipelineId,
|
|
623
|
+
stageId: options.stageId,
|
|
624
|
+
status: options.status === "all" ? undefined : options.status,
|
|
625
|
+
search: options.search,
|
|
626
|
+
limit: options.limit ?? 50,
|
|
627
|
+
offset: (page - 1) * (options.limit ?? 50)
|
|
628
|
+
}),
|
|
629
|
+
crm2.getDealsByStage({ projectId, pipelineId }),
|
|
630
|
+
crm2.getPipelineStages({ pipelineId })
|
|
631
|
+
]);
|
|
632
|
+
setData(dealsResult);
|
|
633
|
+
setDealsByStage(stageDealsResult);
|
|
634
|
+
setStages(stagesResult);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
637
|
+
} finally {
|
|
638
|
+
setLoading(false);
|
|
639
|
+
}
|
|
640
|
+
}, [
|
|
641
|
+
crm2,
|
|
642
|
+
projectId,
|
|
643
|
+
pipelineId,
|
|
644
|
+
options.stageId,
|
|
645
|
+
options.status,
|
|
646
|
+
options.search,
|
|
647
|
+
options.limit,
|
|
648
|
+
page
|
|
649
|
+
]);
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
fetchData();
|
|
652
|
+
}, [fetchData]);
|
|
653
|
+
const stats = useMemo(() => {
|
|
654
|
+
if (!data)
|
|
655
|
+
return null;
|
|
656
|
+
const open = data.deals.filter((d) => d.status === "OPEN");
|
|
657
|
+
const won = data.deals.filter((d) => d.status === "WON");
|
|
658
|
+
const lost = data.deals.filter((d) => d.status === "LOST");
|
|
659
|
+
return {
|
|
660
|
+
total: data.total,
|
|
661
|
+
totalValue: data.totalValue,
|
|
662
|
+
openCount: open.length,
|
|
663
|
+
openValue: open.reduce((sum, d) => sum + d.value, 0),
|
|
664
|
+
wonCount: won.length,
|
|
665
|
+
wonValue: won.reduce((sum, d) => sum + d.value, 0),
|
|
666
|
+
lostCount: lost.length
|
|
667
|
+
};
|
|
668
|
+
}, [data]);
|
|
669
|
+
return {
|
|
670
|
+
data,
|
|
688
671
|
dealsByStage,
|
|
689
|
-
stages
|
|
690
|
-
|
|
672
|
+
stages,
|
|
673
|
+
loading,
|
|
674
|
+
error,
|
|
675
|
+
stats,
|
|
676
|
+
page,
|
|
677
|
+
refetch: fetchData,
|
|
678
|
+
nextPage: () => setPage((p) => p + 1),
|
|
679
|
+
prevPage: () => page > 1 && setPage((p) => p - 1)
|
|
680
|
+
};
|
|
691
681
|
}
|
|
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
682
|
|
|
705
683
|
// src/ui/renderers/pipeline.markdown.ts
|
|
706
684
|
function formatCurrency3(value, currency = "USD") {
|
|
@@ -820,6 +798,28 @@ var crmDashboardMarkdownRenderer = {
|
|
|
820
798
|
};
|
|
821
799
|
}
|
|
822
800
|
};
|
|
801
|
+
|
|
802
|
+
// src/ui/renderers/pipeline.renderer.tsx
|
|
803
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
804
|
+
function CrmPipelineBoardWrapper() {
|
|
805
|
+
const { dealsByStage, stages } = useDealList();
|
|
806
|
+
return /* @__PURE__ */ jsxDEV3(CrmPipelineBoard, {
|
|
807
|
+
dealsByStage,
|
|
808
|
+
stages
|
|
809
|
+
}, undefined, false, undefined, this);
|
|
810
|
+
}
|
|
811
|
+
var crmPipelineReactRenderer = {
|
|
812
|
+
target: "react",
|
|
813
|
+
render: async (desc, _ctx) => {
|
|
814
|
+
if (desc.source.type !== "component") {
|
|
815
|
+
throw new Error("Invalid source type");
|
|
816
|
+
}
|
|
817
|
+
if (desc.source.componentKey !== "CrmPipelineView") {
|
|
818
|
+
throw new Error(`Unknown component: ${desc.source.componentKey}`);
|
|
819
|
+
}
|
|
820
|
+
return /* @__PURE__ */ jsxDEV3(CrmPipelineBoardWrapper, {}, undefined, false, undefined, this);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
823
|
export {
|
|
824
824
|
crmPipelineReactRenderer,
|
|
825
825
|
crmPipelineMarkdownRenderer,
|
|
@@ -1,85 +1,3 @@
|
|
|
1
|
-
// src/ui/hooks/useDealList.ts
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
|
|
4
|
-
"use client";
|
|
5
|
-
function useDealList(options = {}) {
|
|
6
|
-
const { handlers, projectId } = useTemplateRuntime();
|
|
7
|
-
const { crm } = handlers;
|
|
8
|
-
const [data, setData] = useState(null);
|
|
9
|
-
const [dealsByStage, setDealsByStage] = useState({});
|
|
10
|
-
const [stages, setStages] = useState([]);
|
|
11
|
-
const [loading, setLoading] = useState(true);
|
|
12
|
-
const [error, setError] = useState(null);
|
|
13
|
-
const [page, setPage] = useState(1);
|
|
14
|
-
const pipelineId = options.pipelineId ?? "pipeline-1";
|
|
15
|
-
const fetchData = useCallback(async () => {
|
|
16
|
-
setLoading(true);
|
|
17
|
-
setError(null);
|
|
18
|
-
try {
|
|
19
|
-
const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
|
|
20
|
-
crm.listDeals({
|
|
21
|
-
projectId,
|
|
22
|
-
pipelineId,
|
|
23
|
-
stageId: options.stageId,
|
|
24
|
-
status: options.status === "all" ? undefined : options.status,
|
|
25
|
-
search: options.search,
|
|
26
|
-
limit: options.limit ?? 50,
|
|
27
|
-
offset: (page - 1) * (options.limit ?? 50)
|
|
28
|
-
}),
|
|
29
|
-
crm.getDealsByStage({ projectId, pipelineId }),
|
|
30
|
-
crm.getPipelineStages({ pipelineId })
|
|
31
|
-
]);
|
|
32
|
-
setData(dealsResult);
|
|
33
|
-
setDealsByStage(stageDealsResult);
|
|
34
|
-
setStages(stagesResult);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
37
|
-
} finally {
|
|
38
|
-
setLoading(false);
|
|
39
|
-
}
|
|
40
|
-
}, [
|
|
41
|
-
crm,
|
|
42
|
-
projectId,
|
|
43
|
-
pipelineId,
|
|
44
|
-
options.stageId,
|
|
45
|
-
options.status,
|
|
46
|
-
options.search,
|
|
47
|
-
options.limit,
|
|
48
|
-
page
|
|
49
|
-
]);
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
fetchData();
|
|
52
|
-
}, [fetchData]);
|
|
53
|
-
const stats = useMemo(() => {
|
|
54
|
-
if (!data)
|
|
55
|
-
return null;
|
|
56
|
-
const open = data.deals.filter((d) => d.status === "OPEN");
|
|
57
|
-
const won = data.deals.filter((d) => d.status === "WON");
|
|
58
|
-
const lost = data.deals.filter((d) => d.status === "LOST");
|
|
59
|
-
return {
|
|
60
|
-
total: data.total,
|
|
61
|
-
totalValue: data.totalValue,
|
|
62
|
-
openCount: open.length,
|
|
63
|
-
openValue: open.reduce((sum, d) => sum + d.value, 0),
|
|
64
|
-
wonCount: won.length,
|
|
65
|
-
wonValue: won.reduce((sum, d) => sum + d.value, 0),
|
|
66
|
-
lostCount: lost.length
|
|
67
|
-
};
|
|
68
|
-
}, [data]);
|
|
69
|
-
return {
|
|
70
|
-
data,
|
|
71
|
-
dealsByStage,
|
|
72
|
-
stages,
|
|
73
|
-
loading,
|
|
74
|
-
error,
|
|
75
|
-
stats,
|
|
76
|
-
page,
|
|
77
|
-
refetch: fetchData,
|
|
78
|
-
nextPage: () => setPage((p) => p + 1),
|
|
79
|
-
prevPage: () => page > 1 && setPage((p) => p - 1)
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
1
|
// src/ui/CrmDealCard.tsx
|
|
84
2
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
85
3
|
"use client";
|
|
@@ -95,7 +13,7 @@ function CrmDealCard({ deal, onClick }) {
|
|
|
95
13
|
const daysUntilClose = deal.expectedCloseDate ? Math.ceil((deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
|
|
96
14
|
return /* @__PURE__ */ jsxDEV("div", {
|
|
97
15
|
onClick,
|
|
98
|
-
className: "
|
|
16
|
+
className: "cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md",
|
|
99
17
|
role: "button",
|
|
100
18
|
tabIndex: 0,
|
|
101
19
|
onKeyDown: (e) => {
|
|
@@ -104,22 +22,22 @@ function CrmDealCard({ deal, onClick }) {
|
|
|
104
22
|
},
|
|
105
23
|
children: [
|
|
106
24
|
/* @__PURE__ */ jsxDEV("h4", {
|
|
107
|
-
className: "leading-snug
|
|
25
|
+
className: "font-medium leading-snug",
|
|
108
26
|
children: deal.name
|
|
109
27
|
}, undefined, false, undefined, this),
|
|
110
28
|
/* @__PURE__ */ jsxDEV("div", {
|
|
111
|
-
className: "
|
|
29
|
+
className: "mt-2 font-semibold text-lg text-primary",
|
|
112
30
|
children: formatCurrency(deal.value, deal.currency)
|
|
113
31
|
}, undefined, false, undefined, this),
|
|
114
32
|
/* @__PURE__ */ jsxDEV("div", {
|
|
115
|
-
className: "
|
|
33
|
+
className: "mt-3 flex items-center justify-between text-muted-foreground text-xs",
|
|
116
34
|
children: [
|
|
117
35
|
daysUntilClose !== null && /* @__PURE__ */ jsxDEV("span", {
|
|
118
36
|
className: daysUntilClose < 0 ? "text-red-500" : daysUntilClose <= 7 ? "text-yellow-600 dark:text-yellow-500" : "",
|
|
119
37
|
children: daysUntilClose < 0 ? `${Math.abs(daysUntilClose)}d overdue` : daysUntilClose === 0 ? "Due today" : `${daysUntilClose}d left`
|
|
120
38
|
}, undefined, false, undefined, this),
|
|
121
39
|
/* @__PURE__ */ jsxDEV("span", {
|
|
122
|
-
className: `rounded px-1.5 py-0.5 text-xs
|
|
40
|
+
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"}`,
|
|
123
41
|
children: deal.status
|
|
124
42
|
}, undefined, false, undefined, this)
|
|
125
43
|
]
|
|
@@ -129,7 +47,7 @@ function CrmDealCard({ deal, onClick }) {
|
|
|
129
47
|
}
|
|
130
48
|
|
|
131
49
|
// src/ui/CrmPipelineBoard.tsx
|
|
132
|
-
import { useState
|
|
50
|
+
import { useState } from "react";
|
|
133
51
|
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
134
52
|
"use client";
|
|
135
53
|
function formatCurrency2(value) {
|
|
@@ -145,7 +63,7 @@ function CrmPipelineBoard({
|
|
|
145
63
|
onDealClick,
|
|
146
64
|
onDealMove
|
|
147
65
|
}) {
|
|
148
|
-
const [quickMoveOpen, setQuickMoveOpen] =
|
|
66
|
+
const [quickMoveOpen, setQuickMoveOpen] = useState(null);
|
|
149
67
|
const sortedStages = [...stages].sort((a, b) => a.position - b.position);
|
|
150
68
|
const handleQuickMove = (dealId, toStageId) => {
|
|
151
69
|
onDealMove?.(dealId, toStageId);
|
|
@@ -157,10 +75,10 @@ function CrmPipelineBoard({
|
|
|
157
75
|
const deals = dealsByStage[stage.id] ?? [];
|
|
158
76
|
const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
|
|
159
77
|
return /* @__PURE__ */ jsxDEV2("div", {
|
|
160
|
-
className: "
|
|
78
|
+
className: "flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30",
|
|
161
79
|
children: [
|
|
162
80
|
/* @__PURE__ */ jsxDEV2("div", {
|
|
163
|
-
className: "
|
|
81
|
+
className: "flex items-center justify-between border-border border-b px-3 py-2",
|
|
164
82
|
children: [
|
|
165
83
|
/* @__PURE__ */ jsxDEV2("div", {
|
|
166
84
|
children: [
|
|
@@ -179,7 +97,7 @@ function CrmPipelineBoard({
|
|
|
179
97
|
]
|
|
180
98
|
}, undefined, true, undefined, this),
|
|
181
99
|
/* @__PURE__ */ jsxDEV2("span", {
|
|
182
|
-
className: "
|
|
100
|
+
className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
|
|
183
101
|
children: deals.length
|
|
184
102
|
}, undefined, false, undefined, this)
|
|
185
103
|
]
|
|
@@ -187,7 +105,7 @@ function CrmPipelineBoard({
|
|
|
187
105
|
/* @__PURE__ */ jsxDEV2("div", {
|
|
188
106
|
className: "flex flex-1 flex-col gap-2 p-2",
|
|
189
107
|
children: deals.length === 0 ? /* @__PURE__ */ jsxDEV2("div", {
|
|
190
|
-
className: "
|
|
108
|
+
className: "flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs",
|
|
191
109
|
children: "No deals"
|
|
192
110
|
}, undefined, false, undefined, this) : deals.map((deal) => /* @__PURE__ */ jsxDEV2("div", {
|
|
193
111
|
className: "group relative",
|
|
@@ -205,15 +123,15 @@ function CrmPipelineBoard({
|
|
|
205
123
|
e.stopPropagation();
|
|
206
124
|
setQuickMoveOpen(quickMoveOpen === deal.id ? null : deal.id);
|
|
207
125
|
},
|
|
208
|
-
className: "
|
|
126
|
+
className: "flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted",
|
|
209
127
|
title: "Quick move",
|
|
210
128
|
children: "➡️"
|
|
211
129
|
}, undefined, false, undefined, this),
|
|
212
130
|
quickMoveOpen === deal.id && /* @__PURE__ */ jsxDEV2("div", {
|
|
213
|
-
className: "
|
|
131
|
+
className: "absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg",
|
|
214
132
|
children: [
|
|
215
133
|
/* @__PURE__ */ jsxDEV2("p", {
|
|
216
|
-
className: "
|
|
134
|
+
className: "px-3 py-1 font-medium text-muted-foreground text-xs",
|
|
217
135
|
children: "Move to:"
|
|
218
136
|
}, undefined, false, undefined, this),
|
|
219
137
|
sortedStages.filter((s) => s.id !== deal.stageId).map((s) => /* @__PURE__ */ jsxDEV2("button", {
|
|
@@ -222,7 +140,7 @@ function CrmPipelineBoard({
|
|
|
222
140
|
e.stopPropagation();
|
|
223
141
|
handleQuickMove(deal.id, s.id);
|
|
224
142
|
},
|
|
225
|
-
className: "
|
|
143
|
+
className: "w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
|
|
226
144
|
children: s.name
|
|
227
145
|
}, s.id, false, undefined, this))
|
|
228
146
|
]
|
|
@@ -238,6 +156,88 @@ function CrmPipelineBoard({
|
|
|
238
156
|
}, undefined, false, undefined, this);
|
|
239
157
|
}
|
|
240
158
|
|
|
159
|
+
// src/ui/hooks/useDealList.ts
|
|
160
|
+
import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
|
|
161
|
+
import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
|
|
162
|
+
"use client";
|
|
163
|
+
function useDealList(options = {}) {
|
|
164
|
+
const { handlers, projectId } = useTemplateRuntime();
|
|
165
|
+
const { crm } = handlers;
|
|
166
|
+
const [data, setData] = useState2(null);
|
|
167
|
+
const [dealsByStage, setDealsByStage] = useState2({});
|
|
168
|
+
const [stages, setStages] = useState2([]);
|
|
169
|
+
const [loading, setLoading] = useState2(true);
|
|
170
|
+
const [error, setError] = useState2(null);
|
|
171
|
+
const [page, setPage] = useState2(1);
|
|
172
|
+
const pipelineId = options.pipelineId ?? "pipeline-1";
|
|
173
|
+
const fetchData = useCallback(async () => {
|
|
174
|
+
setLoading(true);
|
|
175
|
+
setError(null);
|
|
176
|
+
try {
|
|
177
|
+
const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
|
|
178
|
+
crm.listDeals({
|
|
179
|
+
projectId,
|
|
180
|
+
pipelineId,
|
|
181
|
+
stageId: options.stageId,
|
|
182
|
+
status: options.status === "all" ? undefined : options.status,
|
|
183
|
+
search: options.search,
|
|
184
|
+
limit: options.limit ?? 50,
|
|
185
|
+
offset: (page - 1) * (options.limit ?? 50)
|
|
186
|
+
}),
|
|
187
|
+
crm.getDealsByStage({ projectId, pipelineId }),
|
|
188
|
+
crm.getPipelineStages({ pipelineId })
|
|
189
|
+
]);
|
|
190
|
+
setData(dealsResult);
|
|
191
|
+
setDealsByStage(stageDealsResult);
|
|
192
|
+
setStages(stagesResult);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
195
|
+
} finally {
|
|
196
|
+
setLoading(false);
|
|
197
|
+
}
|
|
198
|
+
}, [
|
|
199
|
+
crm,
|
|
200
|
+
projectId,
|
|
201
|
+
pipelineId,
|
|
202
|
+
options.stageId,
|
|
203
|
+
options.status,
|
|
204
|
+
options.search,
|
|
205
|
+
options.limit,
|
|
206
|
+
page
|
|
207
|
+
]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
fetchData();
|
|
210
|
+
}, [fetchData]);
|
|
211
|
+
const stats = useMemo(() => {
|
|
212
|
+
if (!data)
|
|
213
|
+
return null;
|
|
214
|
+
const open = data.deals.filter((d) => d.status === "OPEN");
|
|
215
|
+
const won = data.deals.filter((d) => d.status === "WON");
|
|
216
|
+
const lost = data.deals.filter((d) => d.status === "LOST");
|
|
217
|
+
return {
|
|
218
|
+
total: data.total,
|
|
219
|
+
totalValue: data.totalValue,
|
|
220
|
+
openCount: open.length,
|
|
221
|
+
openValue: open.reduce((sum, d) => sum + d.value, 0),
|
|
222
|
+
wonCount: won.length,
|
|
223
|
+
wonValue: won.reduce((sum, d) => sum + d.value, 0),
|
|
224
|
+
lostCount: lost.length
|
|
225
|
+
};
|
|
226
|
+
}, [data]);
|
|
227
|
+
return {
|
|
228
|
+
data,
|
|
229
|
+
dealsByStage,
|
|
230
|
+
stages,
|
|
231
|
+
loading,
|
|
232
|
+
error,
|
|
233
|
+
stats,
|
|
234
|
+
page,
|
|
235
|
+
refetch: fetchData,
|
|
236
|
+
nextPage: () => setPage((p) => p + 1),
|
|
237
|
+
prevPage: () => page > 1 && setPage((p) => p - 1)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
241
|
// src/ui/renderers/pipeline.renderer.tsx
|
|
242
242
|
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
243
243
|
function CrmPipelineBoardWrapper() {
|
package/dist/deal/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Deal domain - Deal management in CRM pipeline.
|
|
3
3
|
*/
|
|
4
4
|
export { DealStatusEnum, DealStatusFilterEnum } from './deal.enum';
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
5
|
+
export { CreateDealContract, ListDealsContract, LoseDealContract, MoveDealContract, WinDealContract, } from './deal.operation';
|
|
6
|
+
export { CreateDealInputModel, DealLostPayloadModel, DealModel, DealMovedPayloadModel, DealWonPayloadModel, ListDealsInputModel, ListDealsOutputModel, LoseDealInputModel, MoveDealInputModel, WinDealInputModel, } from './deal.schema';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/events/contact.event.ts
|
|
3
|
-
import { ScalarTypeEnum, defineSchemaModel } from "@contractspec/lib.schema";
|
|
4
3
|
import { defineEvent } from "@contractspec/lib.contracts-spec";
|
|
4
|
+
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
|
|
5
5
|
var ContactCreatedPayload = defineSchemaModel({
|
|
6
6
|
name: "ContactCreatedPayload",
|
|
7
7
|
description: "Payload when a contact is created",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/events/deal.event.ts
|
|
3
|
-
import { ScalarTypeEnum, defineSchemaModel } from "@contractspec/lib.schema";
|
|
4
3
|
import { defineEvent } from "@contractspec/lib.contracts-spec";
|
|
4
|
+
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
|
|
5
5
|
var DealCreatedPayload = defineSchemaModel({
|
|
6
6
|
name: "DealCreatedPayload",
|
|
7
7
|
description: "Payload when a deal is created",
|
package/dist/events/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/events/contact.event.ts
|
|
3
|
-
import { ScalarTypeEnum, defineSchemaModel } from "@contractspec/lib.schema";
|
|
4
3
|
import { defineEvent } from "@contractspec/lib.contracts-spec";
|
|
4
|
+
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
|
|
5
5
|
var ContactCreatedPayload = defineSchemaModel({
|
|
6
6
|
name: "ContactCreatedPayload",
|
|
7
7
|
description: "Payload when a contact is created",
|
|
@@ -29,8 +29,8 @@ var ContactCreatedEvent = defineEvent({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// src/events/deal.event.ts
|
|
32
|
-
import { ScalarTypeEnum as ScalarTypeEnum2, defineSchemaModel as defineSchemaModel2 } from "@contractspec/lib.schema";
|
|
33
32
|
import { defineEvent as defineEvent2 } from "@contractspec/lib.contracts-spec";
|
|
33
|
+
import { defineSchemaModel as defineSchemaModel2, ScalarTypeEnum as ScalarTypeEnum2 } from "@contractspec/lib.schema";
|
|
34
34
|
var DealCreatedPayload = defineSchemaModel2({
|
|
35
35
|
name: "DealCreatedPayload",
|
|
36
36
|
description: "Payload when a deal is created",
|
|
@@ -125,8 +125,8 @@ var DealLostEvent = defineEvent2({
|
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
// src/events/task.event.ts
|
|
128
|
-
import { ScalarTypeEnum as ScalarTypeEnum3, defineSchemaModel as defineSchemaModel3 } from "@contractspec/lib.schema";
|
|
129
128
|
import { defineEvent as defineEvent3 } from "@contractspec/lib.contracts-spec";
|
|
129
|
+
import { defineSchemaModel as defineSchemaModel3, ScalarTypeEnum as ScalarTypeEnum3 } from "@contractspec/lib.schema";
|
|
130
130
|
var TaskCompletedPayload = defineSchemaModel3({
|
|
131
131
|
name: "TaskCompletedPayload",
|
|
132
132
|
description: "Payload when a task is completed",
|