@open-mercato/core 0.6.3-develop.3651.1.d3a548a5f1 → 0.6.3-develop.3660.1.53b73d5aa4
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 +1 -1
- package/dist/generated/entities/customer_settings/index.js +2 -0
- package/dist/generated/entities/customer_settings/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
- package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
- package/dist/modules/customers/api/deals/route.js +255 -122
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
- package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +2 -0
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
- package/dist/modules/customers/commands/deals.js +6 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +48 -13
- package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
- package/dist/modules/customers/commands/settings.js +45 -5
- package/dist/modules/customers/commands/settings.js.map +2 -2
- package/dist/modules/customers/data/enrichers.js +130 -1
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +4 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +29 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/bulkDeals.js +150 -0
- package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js +12 -0
- package/dist/modules/customers/lib/kysely.js.map +7 -0
- package/dist/modules/customers/lib/stuckDeals.js +32 -0
- package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
- package/generated/entities/customer_settings/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
- package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
- package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
- package/src/modules/customers/api/deals/route.ts +330 -135
- package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
- package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
- package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
- package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
- package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
- package/src/modules/customers/commands/deals.ts +19 -0
- package/src/modules/customers/commands/pipeline-stages.ts +68 -13
- package/src/modules/customers/commands/settings.ts +64 -5
- package/src/modules/customers/data/enrichers.ts +209 -2
- package/src/modules/customers/data/entities.ts +4 -1
- package/src/modules/customers/data/validators.ts +36 -2
- package/src/modules/customers/i18n/de.json +244 -0
- package/src/modules/customers/i18n/en.json +244 -0
- package/src/modules/customers/i18n/es.json +244 -0
- package/src/modules/customers/i18n/pl.json +244 -0
- package/src/modules/customers/lib/bulkDeals.ts +231 -0
- package/src/modules/customers/lib/kysely.ts +60 -0
- package/src/modules/customers/lib/stuckDeals.ts +88 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
- package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
- package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
- package/src/modules/customers/workers/deals-bulk-update-stage.ts +46 -0
package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { X } from "lucide-react";
|
|
4
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
5
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
6
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
8
|
+
function FilterPopoverShell({
|
|
9
|
+
title,
|
|
10
|
+
leadingIcon,
|
|
11
|
+
children,
|
|
12
|
+
footerLeft,
|
|
13
|
+
onClose,
|
|
14
|
+
onCancel,
|
|
15
|
+
onApply,
|
|
16
|
+
applyLabel,
|
|
17
|
+
bodyClassName
|
|
18
|
+
}) {
|
|
19
|
+
const t = useT();
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col overflow-hidden rounded-2xl bg-card", children: [
|
|
21
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-border bg-card px-5 py-4", children: [
|
|
22
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
23
|
+
leadingIcon ? /* @__PURE__ */ jsx("span", { className: "flex size-4 items-center justify-center text-muted-foreground", "aria-hidden": "true", children: leadingIcon }) : null,
|
|
24
|
+
/* @__PURE__ */ jsx("span", { className: "text-base font-bold leading-normal text-foreground", children: title })
|
|
25
|
+
] }),
|
|
26
|
+
/* @__PURE__ */ jsx(
|
|
27
|
+
IconButton,
|
|
28
|
+
{
|
|
29
|
+
type: "button",
|
|
30
|
+
variant: "outline",
|
|
31
|
+
size: "sm",
|
|
32
|
+
onClick: onClose,
|
|
33
|
+
"aria-label": translateWithFallback(t, "customers.deals.kanban.filter.close", "Close"),
|
|
34
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": "true" })
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
] }),
|
|
38
|
+
/* @__PURE__ */ jsx("div", { className: bodyClassName ?? "flex flex-col gap-4 bg-card px-5 py-4", children }),
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-border bg-muted px-5 py-3.5", children: [
|
|
40
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs leading-normal text-muted-foreground", children: footerLeft ?? null }),
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-6", children: [
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
Button,
|
|
44
|
+
{
|
|
45
|
+
variant: "outline",
|
|
46
|
+
type: "button",
|
|
47
|
+
onClick: onCancel,
|
|
48
|
+
className: "h-auto rounded-lg border-input bg-card px-4 py-2 text-sm font-semibold text-foreground",
|
|
49
|
+
children: translateWithFallback(t, "customers.deals.kanban.filter.cancel", "Cancel")
|
|
50
|
+
}
|
|
51
|
+
),
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
Button,
|
|
54
|
+
{
|
|
55
|
+
type: "button",
|
|
56
|
+
onClick: onApply,
|
|
57
|
+
className: "h-auto rounded-lg bg-foreground px-5 py-2 text-sm font-semibold text-background hover:bg-foreground/90",
|
|
58
|
+
children: applyLabel ?? translateWithFallback(t, "customers.deals.kanban.filter.apply", "Apply")
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
] })
|
|
62
|
+
] })
|
|
63
|
+
] });
|
|
64
|
+
}
|
|
65
|
+
var FilterPopoverShell_default = FilterPopoverShell;
|
|
66
|
+
export {
|
|
67
|
+
FilterPopoverShell,
|
|
68
|
+
FilterPopoverShell_default as default
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=FilterPopoverShell.js.map
|
package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\n\ntype FilterPopoverShellProps = {\n /** Title rendered in the header (e.g. \"Filter \u00B7 Status\", \"Sort by\") */\n title: React.ReactNode\n /** Optional icon rendered before the title (Sort by uses a tag icon per Figma) */\n leadingIcon?: React.ReactNode\n /** Body content \u2014 sits between header and footer */\n children: React.ReactNode\n /** Footer left content \u2014 usually a \"N selected\" hint or \"Default: \u2026\" subtitle */\n footerLeft?: React.ReactNode\n /** Called when the X close button is clicked */\n onClose: () => void\n /** Called when Cancel is clicked */\n onCancel: () => void\n /** Called when Apply (primary CTA) is clicked */\n onApply: () => void\n /** Override the primary CTA label (e.g. \"Add\" on the +Add-filter popover) */\n applyLabel?: string\n /** Body wrapper className override \u2014 defaults to standard padded white block */\n bodyClassName?: string\n}\n\n/**\n * Shared chrome for every kanban filter popover (Status, Pipeline, Sort, Owner, People,\n * Companies, Close). Matches the SPEC-048 Figma popover surfaces (nodes 1045:11861,\n * 1045:11917, 1045:12090): rounded-2xl outer, white header w/ bold title + close button,\n * white body, muted footer with Cancel + Apply.\n *\n * Each consumer renders its own body \u2014 checkboxes, pills, radio rows, date picker, etc. \u2014\n * inside this shell, so the chrome stays identical across every filter surface and the\n * styling delta lives only inside `children`.\n */\nexport function FilterPopoverShell({\n title,\n leadingIcon,\n children,\n footerLeft,\n onClose,\n onCancel,\n onApply,\n applyLabel,\n bodyClassName,\n}: FilterPopoverShellProps): React.ReactElement {\n const t = useT()\n return (\n <div className=\"flex flex-col overflow-hidden rounded-2xl bg-card\">\n <div className=\"flex items-center justify-between border-b border-border bg-card px-5 py-4\">\n <div className=\"flex items-center gap-2\">\n {leadingIcon ? (\n <span className=\"flex size-4 items-center justify-center text-muted-foreground\" aria-hidden=\"true\">\n {leadingIcon}\n </span>\n ) : null}\n <span className=\"text-base font-bold leading-normal text-foreground\">{title}</span>\n </div>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onClose}\n aria-label={translateWithFallback(t, 'customers.deals.kanban.filter.close', 'Close')}\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </IconButton>\n </div>\n\n <div className={bodyClassName ?? 'flex flex-col gap-4 bg-card px-5 py-4'}>{children}</div>\n\n <div className=\"flex items-center justify-between border-t border-border bg-muted px-5 py-3.5\">\n <div className=\"text-xs leading-normal text-muted-foreground\">{footerLeft ?? null}</div>\n <div className=\"flex items-center gap-6\">\n <Button\n variant=\"outline\"\n type=\"button\"\n onClick={onCancel}\n className=\"h-auto rounded-lg border-input bg-card px-4 py-2 text-sm font-semibold text-foreground\"\n >\n {translateWithFallback(t, 'customers.deals.kanban.filter.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={onApply}\n className=\"h-auto rounded-lg bg-foreground px-5 py-2 text-sm font-semibold text-background hover:bg-foreground/90\"\n >\n {applyLabel ?? translateWithFallback(t, 'customers.deals.kanban.filter.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nexport default FilterPopoverShell\n"],
|
|
5
|
+
"mappings": ";AAuDQ,SAEI,KAFJ;AApDR,SAAS,SAAS;AAClB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,6BAA6B;AAiC/B,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgD;AAC9C,QAAM,IAAI,KAAK;AACf,SACE,qBAAC,SAAI,WAAU,qDACb;AAAA,yBAAC,SAAI,WAAU,8EACb;AAAA,2BAAC,SAAI,WAAU,2BACZ;AAAA,sBACC,oBAAC,UAAK,WAAU,iEAAgE,eAAY,QACzF,uBACH,IACE;AAAA,QACJ,oBAAC,UAAK,WAAU,sDAAsD,iBAAM;AAAA,SAC9E;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS;AAAA,UACT,cAAY,sBAAsB,GAAG,uCAAuC,OAAO;AAAA,UAEnF,8BAAC,KAAE,WAAU,YAAW,eAAY,QAAO;AAAA;AAAA,MAC7C;AAAA,OACF;AAAA,IAEA,oBAAC,SAAI,WAAW,iBAAiB,yCAA0C,UAAS;AAAA,IAEpF,qBAAC,SAAI,WAAU,iFACb;AAAA,0BAAC,SAAI,WAAU,gDAAgD,wBAAc,MAAK;AAAA,MAClF,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAET,gCAAsB,GAAG,wCAAwC,QAAQ;AAAA;AAAA,QAC5E;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAET,wBAAc,sBAAsB,GAAG,uCAAuC,OAAO;AAAA;AAAA,QACxF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;AAEA,IAAO,6BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
5
|
+
import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
|
|
6
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
8
|
+
import { DealCard } from "./DealCard.js";
|
|
9
|
+
import { DashedTileButton } from "./DashedTileButton.js";
|
|
10
|
+
import { LANE_WIDTH_PX } from "./constants.js";
|
|
11
|
+
import { LaneCurrencyBreakdown } from "./LaneCurrencyBreakdown.js";
|
|
12
|
+
import { LaneResizeHandle } from "./LaneResizeHandle.js";
|
|
13
|
+
const ACCENT_TONE_CLASS = {
|
|
14
|
+
success: "bg-status-success-icon",
|
|
15
|
+
error: "bg-status-error-icon",
|
|
16
|
+
warning: "bg-status-warning-icon",
|
|
17
|
+
info: "bg-status-info-icon",
|
|
18
|
+
neutral: "bg-status-neutral-icon",
|
|
19
|
+
brand: "bg-brand-violet",
|
|
20
|
+
pink: "bg-status-pink-icon"
|
|
21
|
+
};
|
|
22
|
+
const COUNT_BADGE_TONE_CLASS = {
|
|
23
|
+
success: "bg-status-success-bg text-status-success-text",
|
|
24
|
+
error: "bg-status-error-bg text-status-error-text",
|
|
25
|
+
warning: "bg-status-warning-bg text-status-warning-text",
|
|
26
|
+
info: "bg-status-info-bg text-status-info-text",
|
|
27
|
+
neutral: "bg-status-neutral-bg text-status-neutral-text",
|
|
28
|
+
brand: "bg-brand-violet/14 text-brand-violet",
|
|
29
|
+
pink: "bg-status-pink-bg text-status-pink-text"
|
|
30
|
+
};
|
|
31
|
+
function getAccentClass(tone) {
|
|
32
|
+
if (tone && tone in ACCENT_TONE_CLASS) return ACCENT_TONE_CLASS[tone];
|
|
33
|
+
return "bg-border";
|
|
34
|
+
}
|
|
35
|
+
function getCountBadgeClass(tone) {
|
|
36
|
+
if (tone && tone in COUNT_BADGE_TONE_CLASS) return COUNT_BADGE_TONE_CLASS[tone];
|
|
37
|
+
return "bg-muted text-muted-foreground";
|
|
38
|
+
}
|
|
39
|
+
function compactCurrency(amount) {
|
|
40
|
+
if (amount >= 1e6) {
|
|
41
|
+
const value = amount / 1e6;
|
|
42
|
+
return value >= 100 ? `${Math.round(value)}M` : `${value.toFixed(1).replace(/\.0$/, "")}M`;
|
|
43
|
+
}
|
|
44
|
+
if (amount >= 1e3) {
|
|
45
|
+
const value = amount / 1e3;
|
|
46
|
+
return value >= 100 ? `${Math.round(value)}k` : `${value.toFixed(1).replace(/\.0$/, "")}k`;
|
|
47
|
+
}
|
|
48
|
+
return String(Math.round(amount));
|
|
49
|
+
}
|
|
50
|
+
function buildLaneStats(deals, aggregate) {
|
|
51
|
+
if (aggregate) {
|
|
52
|
+
const rows2 = aggregate.byCurrency;
|
|
53
|
+
const headline = rows2.length > 0 ? rows2[0] : null;
|
|
54
|
+
return {
|
|
55
|
+
count: aggregate.count,
|
|
56
|
+
primaryAmount: headline ? headline.total : 0,
|
|
57
|
+
primaryCurrency: headline ? headline.currency : null,
|
|
58
|
+
rows: rows2,
|
|
59
|
+
totalInBaseCurrency: aggregate.totalInBaseCurrency,
|
|
60
|
+
baseCurrencyCode: aggregate.baseCurrencyCode,
|
|
61
|
+
convertedAll: aggregate.convertedAll,
|
|
62
|
+
missingRateCurrencies: aggregate.missingRateCurrencies
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const totalsByCurrency = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const deal of deals) {
|
|
67
|
+
if (typeof deal.valueAmount !== "number" || !Number.isFinite(deal.valueAmount)) continue;
|
|
68
|
+
const code = deal.valueCurrency && deal.valueCurrency.length === 3 ? deal.valueCurrency.toUpperCase() : "USD";
|
|
69
|
+
const entry = totalsByCurrency.get(code) ?? { total: 0, count: 0 };
|
|
70
|
+
entry.total += deal.valueAmount;
|
|
71
|
+
entry.count += 1;
|
|
72
|
+
totalsByCurrency.set(code, entry);
|
|
73
|
+
}
|
|
74
|
+
const rows = Array.from(totalsByCurrency.entries()).map(([currency, value]) => ({ currency, total: value.total, count: value.count })).sort((a, b) => b.total - a.total);
|
|
75
|
+
if (rows.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
count: deals.length,
|
|
78
|
+
primaryAmount: 0,
|
|
79
|
+
primaryCurrency: null,
|
|
80
|
+
rows,
|
|
81
|
+
totalInBaseCurrency: 0,
|
|
82
|
+
baseCurrencyCode: null,
|
|
83
|
+
convertedAll: true,
|
|
84
|
+
missingRateCurrencies: []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
count: deals.length,
|
|
89
|
+
primaryAmount: rows[0].total,
|
|
90
|
+
primaryCurrency: rows[0].currency,
|
|
91
|
+
rows,
|
|
92
|
+
totalInBaseCurrency: 0,
|
|
93
|
+
baseCurrencyCode: null,
|
|
94
|
+
// We don't have rates client-side; mark as "not converted" only when there are
|
|
95
|
+
// multiple currencies so the popover can show a "configure FX rates" hint.
|
|
96
|
+
convertedAll: rows.length === 1,
|
|
97
|
+
missingRateCurrencies: rows.length > 1 ? rows.slice(1).map((row) => row.currency) : []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function LaneImpl({
|
|
101
|
+
stage,
|
|
102
|
+
deals,
|
|
103
|
+
aggregate,
|
|
104
|
+
selectedDealIds,
|
|
105
|
+
buildMenuItems,
|
|
106
|
+
activeDragDealId,
|
|
107
|
+
isLoadingMore = false,
|
|
108
|
+
width,
|
|
109
|
+
onToggleSelect,
|
|
110
|
+
onComposeActivity,
|
|
111
|
+
onOpenDetail,
|
|
112
|
+
onQuickAddClick,
|
|
113
|
+
onLoadMore,
|
|
114
|
+
onResize,
|
|
115
|
+
onResizeEnd,
|
|
116
|
+
onResetWidth
|
|
117
|
+
}) {
|
|
118
|
+
const t = useT();
|
|
119
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
120
|
+
id: `lane:${stage.id}`,
|
|
121
|
+
data: { type: "lane", stageId: stage.id }
|
|
122
|
+
});
|
|
123
|
+
const isDragActive = !!activeDragDealId;
|
|
124
|
+
const sourceLane = isDragActive && deals.some((deal) => deal.id === activeDragDealId);
|
|
125
|
+
const showDropHighlight = isOver || isDragActive && !sourceLane;
|
|
126
|
+
const stats = React.useMemo(() => buildLaneStats(deals, aggregate), [deals, aggregate]);
|
|
127
|
+
const bulkSelectionActive = selectedDealIds.size > 0;
|
|
128
|
+
const totalLabel = stats.primaryAmount > 0 && stats.primaryCurrency ? `${compactCurrency(stats.primaryAmount)} ${stats.primaryCurrency}` : null;
|
|
129
|
+
const visibleCount = deals.length;
|
|
130
|
+
const totalCount = stats.count;
|
|
131
|
+
const hasMoreDeals = totalCount > visibleCount;
|
|
132
|
+
const accentClass = getAccentClass(stage.tone);
|
|
133
|
+
const countBadgeClass = getCountBadgeClass(stage.tone);
|
|
134
|
+
const handleQuickAdd = React.useCallback(() => {
|
|
135
|
+
onQuickAddClick(stage.id);
|
|
136
|
+
}, [onQuickAddClick, stage.id]);
|
|
137
|
+
return /* @__PURE__ */ jsxs(
|
|
138
|
+
"div",
|
|
139
|
+
{
|
|
140
|
+
className: "relative flex flex-none flex-col gap-3",
|
|
141
|
+
style: { width: typeof width === "number" && Number.isFinite(width) ? `${width}px` : `${LANE_WIDTH_PX}px` },
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2.5 overflow-clip rounded-lg bg-muted/40 px-4 py-3.5", children: [
|
|
144
|
+
/* @__PURE__ */ jsx("div", { className: `h-1.5 w-full rounded-sm ${accentClass}`, "aria-hidden": "true" }),
|
|
145
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
146
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2.5", children: [
|
|
147
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-bold uppercase leading-normal text-foreground", children: stage.label }),
|
|
148
|
+
/* @__PURE__ */ jsx(
|
|
149
|
+
"span",
|
|
150
|
+
{
|
|
151
|
+
className: `inline-flex shrink-0 items-center rounded-full px-2 py-1 text-xs font-bold leading-normal ${countBadgeClass}`,
|
|
152
|
+
title: hasMoreDeals ? translateWithFallback(
|
|
153
|
+
t,
|
|
154
|
+
"customers.deals.kanban.lane.aria.partialCount",
|
|
155
|
+
"Showing {visible} of {total}",
|
|
156
|
+
{ visible: visibleCount, total: totalCount }
|
|
157
|
+
) : void 0,
|
|
158
|
+
children: totalCount
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
] }),
|
|
162
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
163
|
+
totalLabel ? /* @__PURE__ */ jsx("span", { className: "text-sm font-bold leading-normal text-foreground", children: totalLabel }) : null,
|
|
164
|
+
stats.rows.length > 1 ? /* @__PURE__ */ jsx(
|
|
165
|
+
LaneCurrencyBreakdown,
|
|
166
|
+
{
|
|
167
|
+
rows: stats.rows,
|
|
168
|
+
baseCurrencyCode: stats.baseCurrencyCode,
|
|
169
|
+
totalInBaseCurrency: stats.totalInBaseCurrency,
|
|
170
|
+
convertedAll: stats.convertedAll,
|
|
171
|
+
missingRateCurrencies: stats.missingRateCurrencies,
|
|
172
|
+
triggerLabel: `+${stats.rows.length - 1}`,
|
|
173
|
+
headingLabel: stage.label.toUpperCase(),
|
|
174
|
+
headingCount: totalCount
|
|
175
|
+
}
|
|
176
|
+
) : null
|
|
177
|
+
] })
|
|
178
|
+
] })
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsxs(
|
|
181
|
+
DashedTileButton,
|
|
182
|
+
{
|
|
183
|
+
onClick: handleQuickAdd,
|
|
184
|
+
className: "h-11",
|
|
185
|
+
ariaLabel: translateWithFallback(
|
|
186
|
+
t,
|
|
187
|
+
"customers.deals.kanban.lane.aria.quickAdd",
|
|
188
|
+
"Quickly add a deal to {stage}",
|
|
189
|
+
{ stage: stage.label }
|
|
190
|
+
),
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ jsx("span", { className: "text-base font-bold leading-none", children: "+" }),
|
|
193
|
+
/* @__PURE__ */ jsx("span", { children: translateWithFallback(t, "customers.deals.kanban.cta.quickDeal", "Quick deal") })
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
),
|
|
197
|
+
/* @__PURE__ */ jsxs(
|
|
198
|
+
"div",
|
|
199
|
+
{
|
|
200
|
+
ref: setNodeRef,
|
|
201
|
+
className: `flex min-h-[40vh] flex-col gap-3 rounded-lg p-1.5 ${isOver ? "bg-accent-indigo/10 outline-dashed outline-2 outline-accent-indigo -outline-offset-[2px]" : showDropHighlight ? "bg-muted/40 outline-dashed outline-1 outline-muted-foreground/40 -outline-offset-[2px]" : ""}`,
|
|
202
|
+
children: [
|
|
203
|
+
deals.length === 0 ? /* @__PURE__ */ jsx(
|
|
204
|
+
EmptyState,
|
|
205
|
+
{
|
|
206
|
+
size: "sm",
|
|
207
|
+
title: translateWithFallback(
|
|
208
|
+
t,
|
|
209
|
+
"customers.deals.kanban.lane.empty",
|
|
210
|
+
"No deals in this stage yet."
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
) : deals.map((deal) => /* @__PURE__ */ jsx(
|
|
214
|
+
DealCard,
|
|
215
|
+
{
|
|
216
|
+
deal,
|
|
217
|
+
selected: selectedDealIds.has(deal.id),
|
|
218
|
+
bulkSelectionActive,
|
|
219
|
+
buildMenuItems,
|
|
220
|
+
isActiveDrag: activeDragDealId === deal.id,
|
|
221
|
+
onToggleSelect,
|
|
222
|
+
onComposeActivity,
|
|
223
|
+
onOpenDetail
|
|
224
|
+
},
|
|
225
|
+
deal.id
|
|
226
|
+
)),
|
|
227
|
+
hasMoreDeals && onLoadMore ? /* @__PURE__ */ jsx(
|
|
228
|
+
DashedTileButton,
|
|
229
|
+
{
|
|
230
|
+
onClick: () => onLoadMore(stage.id),
|
|
231
|
+
disabled: isLoadingMore,
|
|
232
|
+
className: "mt-0.75 px-3 py-2.5 text-muted-foreground",
|
|
233
|
+
ariaLabel: translateWithFallback(
|
|
234
|
+
t,
|
|
235
|
+
"customers.deals.kanban.lane.aria.loadMore",
|
|
236
|
+
"Load more deals in {stage}",
|
|
237
|
+
{ stage: stage.label }
|
|
238
|
+
),
|
|
239
|
+
children: isLoadingMore ? translateWithFallback(t, "customers.deals.kanban.lane.loadingMore", "Loading\u2026") : translateWithFallback(
|
|
240
|
+
t,
|
|
241
|
+
"customers.deals.kanban.lane.loadMore",
|
|
242
|
+
"Show more ({remaining})",
|
|
243
|
+
{ remaining: totalCount - visibleCount }
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
) : null
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
onResize ? /* @__PURE__ */ jsx(
|
|
251
|
+
LaneResizeHandle,
|
|
252
|
+
{
|
|
253
|
+
onResize: (delta) => onResize(stage.id, delta),
|
|
254
|
+
onResizeEnd: () => onResizeEnd?.(stage.id),
|
|
255
|
+
onReset: () => onResetWidth?.(stage.id)
|
|
256
|
+
}
|
|
257
|
+
) : null
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const Lane = React.memo(LaneImpl, (prev, next) => {
|
|
263
|
+
if (prev.stage !== next.stage) return false;
|
|
264
|
+
if (prev.deals !== next.deals) return false;
|
|
265
|
+
if (prev.aggregate !== next.aggregate) return false;
|
|
266
|
+
if (prev.selectedDealIds !== next.selectedDealIds) return false;
|
|
267
|
+
if (prev.buildMenuItems !== next.buildMenuItems) return false;
|
|
268
|
+
if (prev.activeDragDealId !== next.activeDragDealId) return false;
|
|
269
|
+
if (prev.isLoadingMore !== next.isLoadingMore) return false;
|
|
270
|
+
if (prev.width !== next.width) return false;
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
var Lane_default = Lane;
|
|
274
|
+
export {
|
|
275
|
+
Lane,
|
|
276
|
+
Lane_default as default
|
|
277
|
+
};
|
|
278
|
+
//# sourceMappingURL=Lane.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useDroppable } from '@dnd-kit/core'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { DealCard, type DealCardData } from './DealCard'\nimport { DashedTileButton } from './DashedTileButton'\nimport { LANE_WIDTH_PX } from './constants'\nimport { LaneCurrencyBreakdown } from './LaneCurrencyBreakdown'\nimport { LaneResizeHandle } from './LaneResizeHandle'\n\nexport type LaneStage = {\n id: string\n label: string\n tone: FilterOptionTone | null\n}\n\nexport type LaneAggregateProp = {\n count: number\n totalInBaseCurrency: number\n byCurrency: Array<{ currency: string; total: number; count: number }>\n baseCurrencyCode: string | null\n /** `true` when every currency in `byCurrency` was convertible to the base currency. */\n convertedAll: boolean\n /** Currencies present in `byCurrency` that lack an FX rate to base \u2014 excluded from totals. */\n missingRateCurrencies: string[]\n}\n\ntype LaneProps = {\n stage: LaneStage\n deals: DealCardData[]\n /** Server-side aggregate for accurate counts/totals across all deals in this stage, not just loaded ones */\n aggregate?: LaneAggregateProp | null\n selectedDealIds: Set<string>\n buildMenuItems: (deal: DealCardData) => RowActionItem[]\n activeDragDealId?: string | null\n isLoadingMore?: boolean\n /** Pixel width override (when the user has resized the column). Falls back to the default class. */\n width?: number | null\n onToggleSelect: (dealId: string) => void\n onComposeActivity: (dealId: string, type: 'call' | 'email' | 'note') => void\n onOpenDetail: (dealId: string) => void\n onQuickAddClick: (stageId: string) => void\n onLoadMore?: (stageId: string) => void\n /** Drag-resize callbacks. When provided, a vertical drag handle is rendered on the lane edge. */\n onResize?: (stageId: string, deltaPx: number) => void\n onResizeEnd?: (stageId: string) => void\n onResetWidth?: (stageId: string) => void\n}\n\n// 4px color bar tone \u2014 uses saturated icon tokens so the bar is visibly colored, not pale\nconst ACCENT_TONE_CLASS: Record<FilterOptionTone, string> = {\n success: 'bg-status-success-icon',\n error: 'bg-status-error-icon',\n warning: 'bg-status-warning-icon',\n info: 'bg-status-info-icon',\n neutral: 'bg-status-neutral-icon',\n brand: 'bg-brand-violet',\n pink: 'bg-status-pink-icon',\n}\n\n// Count pill bg uses very-light status background, text uses status text color\nconst COUNT_BADGE_TONE_CLASS: Record<FilterOptionTone, string> = {\n success: 'bg-status-success-bg text-status-success-text',\n error: 'bg-status-error-bg text-status-error-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n info: 'bg-status-info-bg text-status-info-text',\n neutral: 'bg-status-neutral-bg text-status-neutral-text',\n brand: 'bg-brand-violet/14 text-brand-violet',\n pink: 'bg-status-pink-bg text-status-pink-text',\n}\n\nfunction getAccentClass(tone: FilterOptionTone | null): string {\n if (tone && tone in ACCENT_TONE_CLASS) return ACCENT_TONE_CLASS[tone]\n return 'bg-border'\n}\n\nfunction getCountBadgeClass(tone: FilterOptionTone | null): string {\n if (tone && tone in COUNT_BADGE_TONE_CLASS) return COUNT_BADGE_TONE_CLASS[tone]\n return 'bg-muted text-muted-foreground'\n}\n\nfunction compactCurrency(amount: number): string {\n if (amount >= 1_000_000) {\n const value = amount / 1_000_000\n return value >= 100 ? `${Math.round(value)}M` : `${value.toFixed(1).replace(/\\.0$/, '')}M`\n }\n if (amount >= 1_000) {\n const value = amount / 1_000\n return value >= 100 ? `${Math.round(value)}k` : `${value.toFixed(1).replace(/\\.0$/, '')}k`\n }\n return String(Math.round(amount))\n}\n\ntype CurrencyRow = { currency: string; total: number; count: number }\n\ntype LaneStats = {\n count: number\n /** Headline amount and the currency it is denominated in (largest single currency, raw, never converted). */\n primaryAmount: number\n primaryCurrency: string | null\n /** Per-currency rows sorted desc by total \u2014 the popover renders these one per line. */\n rows: CurrencyRow[]\n /** Sum of `rows` converted to base currency (zero when no base is configured / nothing converted). */\n totalInBaseCurrency: number\n baseCurrencyCode: string | null\n convertedAll: boolean\n missingRateCurrencies: string[]\n}\n\n/**\n * Build display stats.\n * - If a server aggregate is provided, prefer it (covers all deals, converts to tenant base currency).\n * - Otherwise fall back to summing loaded deals (used before aggregate query loads).\n *\n * The headline currency on the lane header is intentionally the LARGEST raw currency\n * (not the base-currency converted total) because the converted total can be misleading\n * when FX rates are missing \u2014 operators see a smaller number than the headline suggests.\n * The base-currency converted total is reserved for the breakdown popover footer, where\n * a partial-conversion indicator can disclose what's excluded.\n */\nfunction buildLaneStats(deals: DealCardData[], aggregate?: LaneAggregateProp | null): LaneStats {\n if (aggregate) {\n const rows = aggregate.byCurrency\n // Pick the single currency with the largest raw total to surface in the header. This\n // is always honest: the operator sees the actual largest cluster of value rather than\n // a synthetic base-currency total that may be partial.\n const headline = rows.length > 0 ? rows[0] : null\n return {\n count: aggregate.count,\n primaryAmount: headline ? headline.total : 0,\n primaryCurrency: headline ? headline.currency : null,\n rows,\n totalInBaseCurrency: aggregate.totalInBaseCurrency,\n baseCurrencyCode: aggregate.baseCurrencyCode,\n convertedAll: aggregate.convertedAll,\n missingRateCurrencies: aggregate.missingRateCurrencies,\n }\n }\n const totalsByCurrency = new Map<string, { total: number; count: number }>()\n for (const deal of deals) {\n if (typeof deal.valueAmount !== 'number' || !Number.isFinite(deal.valueAmount)) continue\n const code = deal.valueCurrency && deal.valueCurrency.length === 3\n ? deal.valueCurrency.toUpperCase()\n : 'USD'\n const entry = totalsByCurrency.get(code) ?? { total: 0, count: 0 }\n entry.total += deal.valueAmount\n entry.count += 1\n totalsByCurrency.set(code, entry)\n }\n const rows: CurrencyRow[] = Array.from(totalsByCurrency.entries())\n .map(([currency, value]) => ({ currency, total: value.total, count: value.count }))\n .sort((a, b) => b.total - a.total)\n if (rows.length === 0) {\n return {\n count: deals.length,\n primaryAmount: 0,\n primaryCurrency: null,\n rows,\n totalInBaseCurrency: 0,\n baseCurrencyCode: null,\n convertedAll: true,\n missingRateCurrencies: [],\n }\n }\n return {\n count: deals.length,\n primaryAmount: rows[0].total,\n primaryCurrency: rows[0].currency,\n rows,\n totalInBaseCurrency: 0,\n baseCurrencyCode: null,\n // We don't have rates client-side; mark as \"not converted\" only when there are\n // multiple currencies so the popover can show a \"configure FX rates\" hint.\n convertedAll: rows.length === 1,\n missingRateCurrencies: rows.length > 1 ? rows.slice(1).map((row) => row.currency) : [],\n }\n}\n\nfunction LaneImpl({\n stage,\n deals,\n aggregate,\n selectedDealIds,\n buildMenuItems,\n activeDragDealId,\n isLoadingMore = false,\n width,\n onToggleSelect,\n onComposeActivity,\n onOpenDetail,\n onQuickAddClick,\n onLoadMore,\n onResize,\n onResizeEnd,\n onResetWidth,\n}: LaneProps): React.ReactElement {\n const t = useT()\n const { setNodeRef, isOver } = useDroppable({\n id: `lane:${stage.id}`,\n data: { type: 'lane', stageId: stage.id },\n })\n const isDragActive = !!activeDragDealId\n const sourceLane = isDragActive && deals.some((deal) => deal.id === activeDragDealId)\n // Highlight drop zone when:\n // - the lane has the pointer over it during a drag, OR\n // - any drag is active and this lane is not the source (so all candidate targets are clearly tagged)\n const showDropHighlight = isOver || (isDragActive && !sourceLane)\n const stats = React.useMemo(() => buildLaneStats(deals, aggregate), [deals, aggregate])\n // Pull boolean out of the Set so each DealCard receives a stable primitive \u2014 that lets\n // React.memo on DealCard skip re-renders when the Set reference changes but its emptiness\n // does not. Flipping false\u2192true (first selection) or true\u2192false (last cleared) forces a\n // full pass over the lane's cards so their checkbox visibility updates atomically.\n const bulkSelectionActive = selectedDealIds.size > 0\n const totalLabel = stats.primaryAmount > 0 && stats.primaryCurrency\n ? `${compactCurrency(stats.primaryAmount)} ${stats.primaryCurrency}`\n : null\n const visibleCount = deals.length\n const totalCount = stats.count\n const hasMoreDeals = totalCount > visibleCount\n const accentClass = getAccentClass(stage.tone)\n const countBadgeClass = getCountBadgeClass(stage.tone)\n\n const handleQuickAdd = React.useCallback(() => {\n onQuickAddClick(stage.id)\n }, [onQuickAddClick, stage.id])\n\n return (\n <div\n className=\"relative flex flex-none flex-col gap-3\"\n style={{ width: typeof width === 'number' && Number.isFinite(width) ? `${width}px` : `${LANE_WIDTH_PX}px` }}\n >\n <div className=\"flex flex-col gap-2.5 overflow-clip rounded-lg bg-muted/40 px-4 py-3.5\">\n <div className={`h-1.5 w-full rounded-sm ${accentClass}`} aria-hidden=\"true\" />\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"truncate text-sm font-bold uppercase leading-normal text-foreground\">\n {stage.label}\n </span>\n <span\n className={`inline-flex shrink-0 items-center rounded-full px-2 py-1 text-xs font-bold leading-normal ${countBadgeClass}`}\n title={\n hasMoreDeals\n ? translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.partialCount',\n 'Showing {visible} of {total}',\n { visible: visibleCount, total: totalCount },\n )\n : undefined\n }\n >\n {totalCount}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {totalLabel ? (\n <span className=\"text-sm font-bold leading-normal text-foreground\">\n {totalLabel}\n </span>\n ) : null}\n {stats.rows.length > 1 ? (\n <LaneCurrencyBreakdown\n rows={stats.rows}\n baseCurrencyCode={stats.baseCurrencyCode}\n totalInBaseCurrency={stats.totalInBaseCurrency}\n convertedAll={stats.convertedAll}\n missingRateCurrencies={stats.missingRateCurrencies}\n triggerLabel={`+${stats.rows.length - 1}`}\n headingLabel={stage.label.toUpperCase()}\n headingCount={totalCount}\n />\n ) : null}\n </div>\n </div>\n </div>\n\n <DashedTileButton\n onClick={handleQuickAdd}\n className=\"h-11\"\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.quickAdd',\n 'Quickly add a deal to {stage}',\n { stage: stage.label },\n )}\n >\n <span className=\"text-base font-bold leading-none\">+</span>\n <span>{translateWithFallback(t, 'customers.deals.kanban.cta.quickDeal', 'Quick deal')}</span>\n </DashedTileButton>\n\n <div\n ref={setNodeRef}\n className={`flex min-h-[40vh] flex-col gap-3 rounded-lg p-1.5 ${\n isOver\n ? 'bg-accent-indigo/10 outline-dashed outline-2 outline-accent-indigo -outline-offset-[2px]'\n : showDropHighlight\n ? 'bg-muted/40 outline-dashed outline-1 outline-muted-foreground/40 -outline-offset-[2px]'\n : ''\n }`}\n >\n {deals.length === 0 ? (\n <EmptyState\n size=\"sm\"\n title={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.empty',\n 'No deals in this stage yet.',\n )}\n />\n ) : (\n deals.map((deal) => (\n <DealCard\n key={deal.id}\n deal={deal}\n selected={selectedDealIds.has(deal.id)}\n bulkSelectionActive={bulkSelectionActive}\n buildMenuItems={buildMenuItems}\n isActiveDrag={activeDragDealId === deal.id}\n onToggleSelect={onToggleSelect}\n onComposeActivity={onComposeActivity}\n onOpenDetail={onOpenDetail}\n />\n ))\n )}\n {hasMoreDeals && onLoadMore ? (\n <DashedTileButton\n onClick={() => onLoadMore(stage.id)}\n disabled={isLoadingMore}\n className=\"mt-0.75 px-3 py-2.5 text-muted-foreground\"\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.loadMore',\n 'Load more deals in {stage}',\n { stage: stage.label },\n )}\n >\n {isLoadingMore\n ? translateWithFallback(t, 'customers.deals.kanban.lane.loadingMore', 'Loading\u2026')\n : translateWithFallback(\n t,\n 'customers.deals.kanban.lane.loadMore',\n 'Show more ({remaining})',\n { remaining: totalCount - visibleCount },\n )}\n </DashedTileButton>\n ) : null}\n </div>\n {onResize ? (\n <LaneResizeHandle\n onResize={(delta) => onResize(stage.id, delta)}\n onResizeEnd={() => onResizeEnd?.(stage.id)}\n onReset={() => onResetWidth?.(stage.id)}\n />\n ) : null}\n </div>\n )\n}\n\n// Memoize the lane so changes in a different lane (or in the active drag) don't re-render us.\n// Page passes booleans (isDragActive) instead of activeDragDealId so most lanes can skip re-rendering.\nexport const Lane = React.memo(LaneImpl, (prev, next) => {\n if (prev.stage !== next.stage) return false\n if (prev.deals !== next.deals) return false\n if (prev.aggregate !== next.aggregate) return false\n if (prev.selectedDealIds !== next.selectedDealIds) return false\n if (prev.buildMenuItems !== next.buildMenuItems) return false\n if (prev.activeDragDealId !== next.activeDragDealId) return false\n if (prev.isLoadingMore !== next.isLoadingMore) return false\n if (prev.width !== next.width) return false\n return true\n})\n\nexport default Lane\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["rows"]
|
|
7
|
+
}
|
package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverContent,
|
|
7
|
+
PopoverTrigger
|
|
8
|
+
} from "@open-mercato/ui/primitives/popover";
|
|
9
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
10
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
11
|
+
import { X } from "lucide-react";
|
|
12
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
13
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
14
|
+
import { CurrencyBreakdownTable } from "./CurrencyBreakdownTable.js";
|
|
15
|
+
function LaneCurrencyBreakdown({
|
|
16
|
+
rows,
|
|
17
|
+
baseCurrencyCode,
|
|
18
|
+
totalInBaseCurrency,
|
|
19
|
+
convertedAll,
|
|
20
|
+
missingRateCurrencies,
|
|
21
|
+
triggerLabel,
|
|
22
|
+
triggerClassName,
|
|
23
|
+
headingLabel,
|
|
24
|
+
headingCount
|
|
25
|
+
}) {
|
|
26
|
+
const t = useT();
|
|
27
|
+
const [open, setOpen] = React.useState(false);
|
|
28
|
+
const closeLabel = translateWithFallback(t, "customers.deals.kanban.filter.close", "Close");
|
|
29
|
+
const titleLabel = translateWithFallback(
|
|
30
|
+
t,
|
|
31
|
+
"customers.deals.kanban.currencyBreakdown.title",
|
|
32
|
+
"Lane breakdown"
|
|
33
|
+
);
|
|
34
|
+
const headingCountLabel = translateWithFallback(
|
|
35
|
+
t,
|
|
36
|
+
"customers.deals.kanban.currencyBreakdown.headingCount",
|
|
37
|
+
"({count} deals)",
|
|
38
|
+
{ count: headingCount }
|
|
39
|
+
);
|
|
40
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
41
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
42
|
+
Button,
|
|
43
|
+
{
|
|
44
|
+
variant: "ghost",
|
|
45
|
+
size: "sm",
|
|
46
|
+
type: "button",
|
|
47
|
+
"aria-label": translateWithFallback(
|
|
48
|
+
t,
|
|
49
|
+
"customers.deals.kanban.lane.currencyBreakdown.trigger",
|
|
50
|
+
"Show per-currency breakdown ({count} currencies)",
|
|
51
|
+
{ count: rows.length }
|
|
52
|
+
),
|
|
53
|
+
className: triggerClassName ?? "inline-flex shrink-0 items-center rounded-full bg-muted px-2 py-1 text-xs font-bold leading-normal text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
54
|
+
children: triggerLabel
|
|
55
|
+
}
|
|
56
|
+
) }),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
PopoverContent,
|
|
59
|
+
{
|
|
60
|
+
align: "end",
|
|
61
|
+
className: "w-80 rounded-2xl border-border bg-transparent p-0 shadow-xl",
|
|
62
|
+
onCloseAutoFocus: (event) => event.preventDefault(),
|
|
63
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col overflow-hidden rounded-2xl bg-card", children: [
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-border bg-card px-5 py-4", children: [
|
|
65
|
+
/* @__PURE__ */ jsx("span", { className: "text-base font-bold leading-normal text-foreground", children: titleLabel }),
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
IconButton,
|
|
68
|
+
{
|
|
69
|
+
variant: "ghost",
|
|
70
|
+
size: "xs",
|
|
71
|
+
onClick: () => setOpen(false),
|
|
72
|
+
"aria-label": closeLabel,
|
|
73
|
+
className: "flex size-7 items-center justify-center rounded-md border border-border bg-card text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
74
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": "true" })
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 bg-card px-6 pt-3.5 pb-2", children: [
|
|
79
|
+
/* @__PURE__ */ jsxs("span", { className: "text-overline font-semibold uppercase tracking-wider text-muted-foreground", children: [
|
|
80
|
+
headingLabel,
|
|
81
|
+
" ",
|
|
82
|
+
headingCountLabel
|
|
83
|
+
] }),
|
|
84
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
85
|
+
/* @__PURE__ */ jsx("span", { className: "text-2xl font-bold leading-normal text-foreground", children: formatAmount(totalInBaseCurrency) }),
|
|
86
|
+
baseCurrencyCode ? /* @__PURE__ */ jsx("span", { className: "text-sm font-normal leading-normal text-muted-foreground", children: baseCurrencyCode }) : null
|
|
87
|
+
] })
|
|
88
|
+
] }),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
CurrencyBreakdownTable,
|
|
91
|
+
{
|
|
92
|
+
rows,
|
|
93
|
+
baseCurrencyCode,
|
|
94
|
+
missingRateCurrencies
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-card px-6 pt-3 pb-3.5", children: [
|
|
98
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-normal leading-normal text-muted-foreground", children: translateWithFallback(
|
|
99
|
+
t,
|
|
100
|
+
"customers.deals.kanban.currencyBreakdown.footerCredit",
|
|
101
|
+
"NBP mid rate \xB7 {date}",
|
|
102
|
+
{ date: formatToday() }
|
|
103
|
+
) }),
|
|
104
|
+
!convertedAll && missingRateCurrencies.length > 0 ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs leading-normal text-muted-foreground", children: translateWithFallback(
|
|
105
|
+
t,
|
|
106
|
+
"customers.deals.kanban.lane.currencyBreakdown.missingRatesHint",
|
|
107
|
+
"Missing FX rates for {currencies} \u2014 excluded from total. Configure exchange rates to see the full converted value.",
|
|
108
|
+
{ currencies: missingRateCurrencies.join(", ") }
|
|
109
|
+
) }) : null
|
|
110
|
+
] })
|
|
111
|
+
] })
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
] });
|
|
115
|
+
}
|
|
116
|
+
function formatAmount(amount) {
|
|
117
|
+
return new Intl.NumberFormat(void 0, {
|
|
118
|
+
style: "decimal",
|
|
119
|
+
maximumFractionDigits: 0,
|
|
120
|
+
useGrouping: true
|
|
121
|
+
}).format(Math.round(amount));
|
|
122
|
+
}
|
|
123
|
+
function formatToday() {
|
|
124
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
125
|
+
year: "numeric",
|
|
126
|
+
month: "short",
|
|
127
|
+
day: "2-digit"
|
|
128
|
+
}).format(/* @__PURE__ */ new Date());
|
|
129
|
+
}
|
|
130
|
+
var LaneCurrencyBreakdown_default = LaneCurrencyBreakdown;
|
|
131
|
+
export {
|
|
132
|
+
LaneCurrencyBreakdown,
|
|
133
|
+
LaneCurrencyBreakdown_default as default
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=LaneCurrencyBreakdown.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { X } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { CurrencyBreakdownTable, type CurrencyBreakdownRow } from './CurrencyBreakdownTable'\n\ntype LaneCurrencyBreakdownProps = {\n /** Per-currency breakdown rows (already sorted desc by total in the API response). */\n rows: CurrencyBreakdownRow[]\n /** Tenant base currency code, or `null` when none is configured. */\n baseCurrencyCode: string | null\n /** Sum of `rows` converted to the base currency (only rows with a usable FX rate are included). */\n totalInBaseCurrency: number\n /** Whether every row in `rows` was either already base-currency or had an FX rate. */\n convertedAll: boolean\n /** Currencies in `rows` that have no FX rate to base \u2014 they are excluded from `totalInBaseCurrency`. */\n missingRateCurrencies: string[]\n /** Visible chip label shown in the lane header (e.g. \"+3\"). */\n triggerLabel: string\n /** Optional className for the trigger chip. */\n triggerClassName?: string\n /**\n * Heading shown above the table (e.g. lane stage label or `\"PIPELINE\"` for the\n * board-level breakdown). Rendered in overline style above the headline total.\n */\n headingLabel: string\n /** Total deal count shown next to the heading label (e.g. `\"49 deals\"`). */\n headingCount: number\n}\n\n/**\n * Read-only per-currency breakdown popover.\n *\n * Reused by:\n * - lane header `+N` chip (lane-scoped)\n * - page-level Breakdown button (pipeline-scoped)\n *\n * Visual design from SPEC-048 Figma node 1251:610 \u2014 bordered white header bar with bold title +\n * close button, overline heading + large total amount, currency table with thin row dividers,\n * BASE pill on the base-currency row, and a muted \"NBP mid rate \u00B7 {date}\" footer credit.\n *\n * The body (column header + currency rows + BASE pill + footer credit) lives in\n * `CurrencyBreakdownTable` so the filter variant (`CurrencyFilterPopover`) can reuse the same\n * row composition without duplicating styling.\n */\nexport function LaneCurrencyBreakdown({\n rows,\n baseCurrencyCode,\n totalInBaseCurrency,\n convertedAll,\n missingRateCurrencies,\n triggerLabel,\n triggerClassName,\n headingLabel,\n headingCount,\n}: LaneCurrencyBreakdownProps): React.ReactElement {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n\n const closeLabel = translateWithFallback(t, 'customers.deals.kanban.filter.close', 'Close')\n const titleLabel = translateWithFallback(\n t,\n 'customers.deals.kanban.currencyBreakdown.title',\n 'Lane breakdown',\n )\n const headingCountLabel = translateWithFallback(\n t,\n 'customers.deals.kanban.currencyBreakdown.headingCount',\n '({count} deals)',\n { count: headingCount },\n )\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n aria-label={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.currencyBreakdown.trigger',\n 'Show per-currency breakdown ({count} currencies)',\n { count: rows.length },\n )}\n className={\n triggerClassName ??\n 'inline-flex shrink-0 items-center rounded-full bg-muted px-2 py-1 text-xs font-bold leading-normal text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'\n }\n >\n {triggerLabel}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n className=\"w-80 rounded-2xl border-border bg-transparent p-0 shadow-xl\"\n onCloseAutoFocus={(event) => event.preventDefault()}\n >\n {/*\n Solid `bg-card` (no opacity modifier) on the outer wrapper: floating overlays must not\n let underlying card content bleed through. Round-2 UX review item 33 \u2014 same class of\n bug as item 6 in the shared FilterPopoverShell, but in popovers that hand-roll their\n chrome instead of using the shell.\n */}\n <div className=\"flex flex-col overflow-hidden rounded-2xl bg-card\">\n <div className=\"flex items-center justify-between border-b border-border bg-card px-5 py-4\">\n <span className=\"text-base font-bold leading-normal text-foreground\">{titleLabel}</span>\n <IconButton\n variant=\"ghost\"\n size=\"xs\"\n onClick={() => setOpen(false)}\n aria-label={closeLabel}\n className=\"flex size-7 items-center justify-center rounded-md border border-border bg-card text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n >\n <X className=\"size-3.5\" aria-hidden=\"true\" />\n </IconButton>\n </div>\n\n <div className=\"flex flex-col gap-1.5 bg-card px-6 pt-3.5 pb-2\">\n <span className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground\">\n {headingLabel} {headingCountLabel}\n </span>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold leading-normal text-foreground\">\n {formatAmount(totalInBaseCurrency)}\n </span>\n {baseCurrencyCode ? (\n <span className=\"text-sm font-normal leading-normal text-muted-foreground\">\n {baseCurrencyCode}\n </span>\n ) : null}\n </div>\n </div>\n\n <CurrencyBreakdownTable\n rows={rows}\n baseCurrencyCode={baseCurrencyCode}\n missingRateCurrencies={missingRateCurrencies}\n />\n\n <div className=\"bg-card px-6 pt-3 pb-3.5\">\n <p className=\"text-xs font-normal leading-normal text-muted-foreground\">\n {translateWithFallback(\n t,\n 'customers.deals.kanban.currencyBreakdown.footerCredit',\n 'NBP mid rate \u00B7 {date}',\n { date: formatToday() },\n )}\n </p>\n {!convertedAll && missingRateCurrencies.length > 0 ? (\n <p className=\"mt-2 text-xs leading-normal text-muted-foreground\">\n {translateWithFallback(\n t,\n 'customers.deals.kanban.lane.currencyBreakdown.missingRatesHint',\n 'Missing FX rates for {currencies} \u2014 excluded from total. Configure exchange rates to see the full converted value.',\n { currencies: missingRateCurrencies.join(', ') },\n )}\n </p>\n ) : null}\n </div>\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction formatAmount(amount: number): string {\n return new Intl.NumberFormat(undefined, {\n style: 'decimal',\n maximumFractionDigits: 0,\n useGrouping: true,\n }).format(Math.round(amount))\n}\n\nfunction formatToday(): string {\n return new Intl.DateTimeFormat(undefined, {\n year: 'numeric',\n month: 'short',\n day: '2-digit',\n }).format(new Date())\n}\n\nexport default LaneCurrencyBreakdown\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|