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