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