@open-mercato/core 0.6.6-develop.6385.1.9a81faa5f0 → 0.6.6-develop.6387.1.f4896ad565
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/modules/attachments/lib/scope.js +26 -0
- package/dist/modules/attachments/lib/scope.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +4 -2
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/api/deals/map/route.js +433 -0
- package/dist/modules/customers/api/deals/map/route.js.map +7 -0
- package/dist/modules/customers/backend/customers/companies/create/page.js +2 -0
- package/dist/modules/customers/backend/customers/companies/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.js +159 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.js +26 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js +321 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js +533 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/page.js +55 -0
- package/dist/modules/customers/backend/customers/deals/map/page.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/page.meta.js +38 -0
- package/dist/modules/customers/backend/customers/deals/map/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +3 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +22 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/create/page.js +2 -0
- package/dist/modules/customers/backend/customers/people/create/page.js.map +2 -2
- package/dist/modules/customers/components/AddressEditor.js +34 -2
- package/dist/modules/customers/components/AddressEditor.js.map +2 -2
- package/dist/modules/customers/components/AddressTiles.js +43 -3
- package/dist/modules/customers/components/AddressTiles.js.map +2 -2
- package/dist/modules/customers/components/detail/AddressesSection.js +4 -1
- package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +10 -0
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/dealsMapLocation.js +75 -0
- package/dist/modules/customers/lib/dealsMapLocation.js.map +7 -0
- package/dist/modules/messages/commands/messages.js +0 -1
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/lib/attachments.js +13 -7
- package/dist/modules/messages/lib/attachments.js.map +2 -2
- package/package.json +11 -7
- package/src/modules/attachments/lib/scope.ts +42 -0
- package/src/modules/catalog/commands/variants.ts +7 -2
- package/src/modules/customers/api/deals/map/route.ts +549 -0
- package/src/modules/customers/backend/customers/companies/create/page.tsx +2 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.tsx +213 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.tsx +79 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.tsx +407 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapView.tsx +667 -0
- package/src/modules/customers/backend/customers/deals/map/page.meta.ts +36 -0
- package/src/modules/customers/backend/customers/deals/map/page.tsx +77 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +3 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +22 -1
- package/src/modules/customers/backend/customers/people/create/page.tsx +2 -0
- package/src/modules/customers/components/AddressEditor.tsx +35 -0
- package/src/modules/customers/components/AddressTiles.tsx +51 -1
- package/src/modules/customers/components/detail/AddressesSection.tsx +3 -0
- package/src/modules/customers/components/formConfig.tsx +10 -0
- package/src/modules/customers/data/validators.ts +13 -2
- package/src/modules/customers/i18n/de.json +24 -0
- package/src/modules/customers/i18n/en.json +24 -0
- package/src/modules/customers/i18n/es.json +24 -0
- package/src/modules/customers/i18n/pl.json +24 -0
- package/src/modules/customers/lib/dealsMapLocation.ts +125 -0
- package/src/modules/messages/commands/messages.ts +0 -1
- package/src/modules/messages/lib/attachments.ts +15 -7
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { MapPin, MapPinOff } from "lucide-react";
|
|
5
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
|
+
import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue
|
|
13
|
+
} from "@open-mercato/ui/primitives/select";
|
|
14
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
15
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
16
|
+
import { formatCurrency } from "../../../../../components/detail/utils.js";
|
|
17
|
+
const STAGE_BADGE_TONE_CLASS = {
|
|
18
|
+
success: "bg-status-success-bg text-status-success-text",
|
|
19
|
+
error: "bg-status-error-bg text-status-error-text",
|
|
20
|
+
warning: "bg-status-warning-bg text-status-warning-text",
|
|
21
|
+
info: "bg-status-info-bg text-status-info-text",
|
|
22
|
+
neutral: "bg-status-neutral-bg text-status-neutral-text",
|
|
23
|
+
brand: "bg-brand-violet/14 text-brand-violet",
|
|
24
|
+
pink: "bg-status-pink-bg text-status-pink-text"
|
|
25
|
+
};
|
|
26
|
+
function getStageBadgeClass(tone) {
|
|
27
|
+
if (tone && tone in STAGE_BADGE_TONE_CLASS) return STAGE_BADGE_TONE_CLASS[tone];
|
|
28
|
+
return "bg-muted text-muted-foreground";
|
|
29
|
+
}
|
|
30
|
+
function haversineKm(a, b) {
|
|
31
|
+
const toRadians = (value) => value * Math.PI / 180;
|
|
32
|
+
const earthRadiusKm = 6371;
|
|
33
|
+
const dLat = toRadians(b.latitude - a.latitude);
|
|
34
|
+
const dLng = toRadians(b.longitude - a.longitude);
|
|
35
|
+
const sinLat = Math.sin(dLat / 2);
|
|
36
|
+
const sinLng = Math.sin(dLng / 2);
|
|
37
|
+
const h = sinLat * sinLat + Math.cos(toRadians(a.latitude)) * Math.cos(toRadians(b.latitude)) * sinLng * sinLng;
|
|
38
|
+
return 2 * earthRadiusKm * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
39
|
+
}
|
|
40
|
+
function DealsLocationPanel({
|
|
41
|
+
deals,
|
|
42
|
+
locatedCount,
|
|
43
|
+
stageMetaById,
|
|
44
|
+
mapCenter,
|
|
45
|
+
selectedDealId,
|
|
46
|
+
onSelect
|
|
47
|
+
}) {
|
|
48
|
+
const t = useT();
|
|
49
|
+
const [panelSort, setPanelSort] = React.useState("proximity");
|
|
50
|
+
const visibleDeals = React.useMemo(() => {
|
|
51
|
+
if (panelSort !== "proximity" || !mapCenter) return deals;
|
|
52
|
+
return deals.slice().sort((a, b) => {
|
|
53
|
+
if (!a.location) return 1;
|
|
54
|
+
if (!b.location) return -1;
|
|
55
|
+
return haversineKm(mapCenter, a.location) - haversineKm(mapCenter, b.location);
|
|
56
|
+
});
|
|
57
|
+
}, [deals, panelSort, mapCenter]);
|
|
58
|
+
const noAddressLabel = translateWithFallback(
|
|
59
|
+
t,
|
|
60
|
+
"customers.deals.map.panel.noAddress",
|
|
61
|
+
"No address coordinates"
|
|
62
|
+
);
|
|
63
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex max-h-[60vh] w-full shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-card lg:max-h-none lg:w-80", children: [
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 border-b border-border px-4 py-3", children: [
|
|
65
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
66
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold leading-normal text-foreground", children: translateWithFallback(t, "customers.deals.map.panel.title", "Deals with location") }),
|
|
67
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs leading-normal text-muted-foreground", children: translateWithFallback(t, "customers.deals.map.panel.count", "{count} located", {
|
|
68
|
+
count: locatedCount
|
|
69
|
+
}) })
|
|
70
|
+
] }),
|
|
71
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs leading-normal text-muted-foreground", children: translateWithFallback(
|
|
72
|
+
t,
|
|
73
|
+
"customers.deals.map.panel.hint",
|
|
74
|
+
"Click a deal or a map pin to preview it."
|
|
75
|
+
) }),
|
|
76
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 flex items-center gap-2", children: /* @__PURE__ */ jsxs(Select, { value: panelSort, onValueChange: (value) => setPanelSort(value), children: [
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
SelectTrigger,
|
|
79
|
+
{
|
|
80
|
+
size: "sm",
|
|
81
|
+
className: "w-auto min-w-36",
|
|
82
|
+
"aria-label": translateWithFallback(t, "customers.deals.map.panel.sort.label", "Sort deals panel"),
|
|
83
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
87
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "proximity", children: translateWithFallback(t, "customers.deals.map.panel.sort.proximity", "Closest first") }),
|
|
88
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "listOrder", children: translateWithFallback(t, "customers.deals.map.panel.sort.listOrder", "List order") })
|
|
89
|
+
] })
|
|
90
|
+
] }) })
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-2 overflow-y-auto p-3", children: [
|
|
93
|
+
deals.length === 0 ? /* @__PURE__ */ jsx(
|
|
94
|
+
EmptyState,
|
|
95
|
+
{
|
|
96
|
+
variant: "subtle",
|
|
97
|
+
size: "sm",
|
|
98
|
+
icon: /* @__PURE__ */ jsx(MapPinOff, { className: "size-6", "aria-hidden": "true" }),
|
|
99
|
+
title: translateWithFallback(
|
|
100
|
+
t,
|
|
101
|
+
"customers.deals.map.panel.empty.title",
|
|
102
|
+
"No deals on the map yet"
|
|
103
|
+
),
|
|
104
|
+
description: translateWithFallback(
|
|
105
|
+
t,
|
|
106
|
+
"customers.deals.map.panel.empty.description",
|
|
107
|
+
"Add latitude and longitude to company or person addresses to plot their deals here."
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
) : null,
|
|
111
|
+
visibleDeals.map((deal) => {
|
|
112
|
+
const stageMeta = deal.pipelineStageId ? stageMetaById.get(deal.pipelineStageId) ?? null : null;
|
|
113
|
+
const isSelected = deal.id === selectedDealId;
|
|
114
|
+
const locationLine = deal.location ? [deal.location.city, deal.location.region].filter(Boolean).join(", ") || deal.location.country || null : null;
|
|
115
|
+
const valueLabel = typeof deal.valueAmount === "number" ? formatCurrency(deal.valueAmount, deal.valueCurrency) : null;
|
|
116
|
+
const cardAriaLabel = [deal.companyLabel, deal.title, valueLabel, stageMeta?.label, locationLine ?? noAddressLabel].filter((part) => typeof part === "string" && part.length > 0).join(", ") || deal.title;
|
|
117
|
+
return /* @__PURE__ */ jsxs(
|
|
118
|
+
Button,
|
|
119
|
+
{
|
|
120
|
+
type: "button",
|
|
121
|
+
variant: "ghost",
|
|
122
|
+
"data-map-panel-card": deal.id,
|
|
123
|
+
"aria-label": cardAriaLabel,
|
|
124
|
+
"aria-pressed": isSelected,
|
|
125
|
+
onClick: () => onSelect(deal.id),
|
|
126
|
+
className: `h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-lg border border-border p-3 text-left shadow-none ${isSelected ? "bg-muted" : "bg-card"}`,
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium leading-normal text-foreground", children: deal.companyLabel ?? "\u2014" }),
|
|
130
|
+
valueLabel ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold leading-normal text-foreground", children: valueLabel }) : null
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm leading-normal text-muted-foreground", children: deal.title }),
|
|
133
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
134
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs leading-normal text-muted-foreground", children: [
|
|
135
|
+
/* @__PURE__ */ jsx(MapPin, { className: "size-3.5 shrink-0", "aria-hidden": "true" }),
|
|
136
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: locationLine ?? noAddressLabel })
|
|
137
|
+
] }),
|
|
138
|
+
stageMeta ? /* @__PURE__ */ jsx(
|
|
139
|
+
"span",
|
|
140
|
+
{
|
|
141
|
+
className: `inline-flex shrink-0 items-center rounded-md px-2 py-0.5 text-xs font-semibold leading-normal ${getStageBadgeClass(stageMeta.tone)}`,
|
|
142
|
+
children: stageMeta.label
|
|
143
|
+
}
|
|
144
|
+
) : null
|
|
145
|
+
] })
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
deal.id
|
|
149
|
+
);
|
|
150
|
+
})
|
|
151
|
+
] })
|
|
152
|
+
] });
|
|
153
|
+
}
|
|
154
|
+
var DealsLocationPanel_default = DealsLocationPanel;
|
|
155
|
+
export {
|
|
156
|
+
DealsLocationPanel,
|
|
157
|
+
DealsLocationPanel_default as default
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=DealsLocationPanel.js.map
|
package/dist/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { MapPin, MapPinOff } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\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 { formatCurrency } from '../../../../../components/detail/utils'\nimport type { MapDeal, StageMeta } from './DealsMapView'\nimport type { MapCenter } from './DealsMapCanvas'\n\nconst STAGE_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 getStageBadgeClass(tone: FilterOptionTone | null): string {\n if (tone && tone in STAGE_BADGE_TONE_CLASS) return STAGE_BADGE_TONE_CLASS[tone]\n return 'bg-muted text-muted-foreground'\n}\n\ntype PanelSort = 'proximity' | 'listOrder'\n\nfunction haversineKm(\n a: { latitude: number; longitude: number },\n b: { latitude: number; longitude: number },\n): number {\n const toRadians = (value: number) => (value * Math.PI) / 180\n const earthRadiusKm = 6371\n const dLat = toRadians(b.latitude - a.latitude)\n const dLng = toRadians(b.longitude - a.longitude)\n const sinLat = Math.sin(dLat / 2)\n const sinLng = Math.sin(dLng / 2)\n const h =\n sinLat * sinLat +\n Math.cos(toRadians(a.latitude)) * Math.cos(toRadians(b.latitude)) * sinLng * sinLng\n return 2 * earthRadiusKm * Math.asin(Math.min(1, Math.sqrt(h)))\n}\n\ntype DealsLocationPanelProps = {\n deals: MapDeal[]\n // Total deals matching the active filters that resolve to a location (the located-only endpoint's\n // `total`). May exceed `deals.length` when the client cap truncates the rendered set.\n locatedCount: number\n stageMetaById: Map<string, StageMeta>\n mapCenter: MapCenter | null\n selectedDealId: string | null\n onSelect: (dealId: string | null) => void\n}\n\nexport function DealsLocationPanel({\n deals,\n locatedCount,\n stageMetaById,\n mapCenter,\n selectedDealId,\n onSelect,\n}: DealsLocationPanelProps): React.ReactElement {\n const t = useT()\n const [panelSort, setPanelSort] = React.useState<PanelSort>('proximity')\n\n // The map endpoint is located-only, so every deal here carries a location. The guard keeps the\n // proximity comparator total-order-safe against the nullable `MapDeal.location` type.\n const visibleDeals = React.useMemo(() => {\n if (panelSort !== 'proximity' || !mapCenter) return deals\n return deals.slice().sort((a, b) => {\n if (!a.location) return 1\n if (!b.location) return -1\n return haversineKm(mapCenter, a.location) - haversineKm(mapCenter, b.location)\n })\n }, [deals, panelSort, mapCenter])\n\n const noAddressLabel = translateWithFallback(\n t,\n 'customers.deals.map.panel.noAddress',\n 'No address coordinates',\n )\n\n return (\n <div className=\"flex max-h-[60vh] w-full shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-card lg:max-h-none lg:w-80\">\n <div className=\"flex flex-col gap-1 border-b border-border px-4 py-3\">\n <div className=\"flex items-baseline justify-between gap-2\">\n <h2 className=\"text-sm font-semibold leading-normal text-foreground\">\n {translateWithFallback(t, 'customers.deals.map.panel.title', 'Deals with location')}\n </h2>\n <span className=\"text-xs leading-normal text-muted-foreground\">\n {translateWithFallback(t, 'customers.deals.map.panel.count', '{count} located', {\n count: locatedCount,\n })}\n </span>\n </div>\n <p className=\"text-xs leading-normal text-muted-foreground\">\n {translateWithFallback(\n t,\n 'customers.deals.map.panel.hint',\n 'Click a deal or a map pin to preview it.',\n )}\n </p>\n <div className=\"mt-2 flex items-center gap-2\">\n <Select value={panelSort} onValueChange={(value) => setPanelSort(value as PanelSort)}>\n <SelectTrigger\n size=\"sm\"\n className=\"w-auto min-w-36\"\n aria-label={translateWithFallback(t, 'customers.deals.map.panel.sort.label', 'Sort deals panel')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"proximity\">\n {translateWithFallback(t, 'customers.deals.map.panel.sort.proximity', 'Closest first')}\n </SelectItem>\n <SelectItem value=\"listOrder\">\n {translateWithFallback(t, 'customers.deals.map.panel.sort.listOrder', 'List order')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"flex flex-1 flex-col gap-2 overflow-y-auto p-3\">\n {deals.length === 0 ? (\n <EmptyState\n variant=\"subtle\"\n size=\"sm\"\n icon={<MapPinOff className=\"size-6\" aria-hidden=\"true\" />}\n title={translateWithFallback(\n t,\n 'customers.deals.map.panel.empty.title',\n 'No deals on the map yet',\n )}\n description={translateWithFallback(\n t,\n 'customers.deals.map.panel.empty.description',\n 'Add latitude and longitude to company or person addresses to plot their deals here.',\n )}\n />\n ) : null}\n {visibleDeals.map((deal) => {\n const stageMeta = deal.pipelineStageId ? stageMetaById.get(deal.pipelineStageId) ?? null : null\n const isSelected = deal.id === selectedDealId\n const locationLine = deal.location\n ? [deal.location.city, deal.location.region].filter(Boolean).join(', ') ||\n deal.location.country ||\n null\n : null\n const valueLabel =\n typeof deal.valueAmount === 'number' ? formatCurrency(deal.valueAmount, deal.valueCurrency) : null\n // Compose the full card content for screen readers \u2014 the bare title alone dropped the\n // company, value, stage and location that sighted users read off the card.\n const cardAriaLabel =\n [deal.companyLabel, deal.title, valueLabel, stageMeta?.label, locationLine ?? noAddressLabel]\n .filter((part): part is string => typeof part === 'string' && part.length > 0)\n .join(', ') || deal.title\n return (\n <Button\n key={deal.id}\n type=\"button\"\n variant=\"ghost\"\n data-map-panel-card={deal.id}\n aria-label={cardAriaLabel}\n aria-pressed={isSelected}\n onClick={() => onSelect(deal.id)}\n className={`h-auto w-full flex-col items-stretch justify-start gap-1 whitespace-normal rounded-lg border border-border p-3 text-left shadow-none ${\n isSelected ? 'bg-muted' : 'bg-card'\n }`}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate text-sm font-medium leading-normal text-foreground\">\n {deal.companyLabel ?? '\u2014'}\n </span>\n {valueLabel ? (\n <span className=\"shrink-0 text-sm font-semibold leading-normal text-foreground\">\n {valueLabel}\n </span>\n ) : null}\n </div>\n <span className=\"truncate text-sm leading-normal text-muted-foreground\">{deal.title}</span>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-xs leading-normal text-muted-foreground\">\n <MapPin className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span className=\"truncate\">{locationLine ?? noAddressLabel}</span>\n </span>\n {stageMeta ? (\n <span\n className={`inline-flex shrink-0 items-center rounded-md px-2 py-0.5 text-xs font-semibold leading-normal ${getStageBadgeClass(stageMeta.tone)}`}\n >\n {stageMeta.label}\n </span>\n ) : null}\n </div>\n </Button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport default DealsLocationPanel\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
5
|
+
function DealsMapCanvas(props) {
|
|
6
|
+
const [Impl, setImpl] = React.useState(null);
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
void import("./DealsMapCanvasImpl.js").then((mod) => {
|
|
10
|
+
if (!cancelled) setImpl(() => mod.default);
|
|
11
|
+
});
|
|
12
|
+
return () => {
|
|
13
|
+
cancelled = true;
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
if (!Impl) {
|
|
17
|
+
return /* @__PURE__ */ jsx("div", { className: `flex items-center justify-center bg-muted/30 ${props.className ?? ""}`.trim(), children: /* @__PURE__ */ jsx(Spinner, { className: "size-6 text-muted-foreground" }) });
|
|
18
|
+
}
|
|
19
|
+
return /* @__PURE__ */ jsx(Impl, { ...props });
|
|
20
|
+
}
|
|
21
|
+
var DealsMapCanvas_default = DealsMapCanvas;
|
|
22
|
+
export {
|
|
23
|
+
DealsMapCanvas,
|
|
24
|
+
DealsMapCanvas_default as default
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=DealsMapCanvas.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'\n\nexport type MapCenter = {\n latitude: number\n longitude: number\n}\n\nexport type DealsMapCanvasDeal = {\n id: string\n latitude: number\n longitude: number\n tone: FilterOptionTone | null\n}\n\nexport type DealsMapLegendStage = {\n id: string\n label: string\n tone: FilterOptionTone | null\n}\n\nexport type DealsMapPreview = {\n id: string\n title: string\n companyLabel: string | null\n locationLine: string | null\n valueAmount: number | null\n valueCurrency: string | null\n probability: number | null\n expectedCloseAt: string | null\n stageLabel: string | null\n stageTone: FilterOptionTone | null\n ownerName: string | null\n}\n\nexport type DealsMapCanvasProps = {\n deals: DealsMapCanvasDeal[]\n legendStages: DealsMapLegendStage[]\n preview: DealsMapPreview | null\n selectedDealId: string | null\n onSelect: (dealId: string | null) => void\n onCenterChange: (center: MapCenter) => void\n className?: string\n}\n\ntype DealsMapCanvasComponent = (props: DealsMapCanvasProps) => React.ReactElement\n\n/**\n * Lazy shell around the Leaflet implementation. Leaflet touches `window` at import time and ships\n * its own CSS, so the impl chunk (JS + stylesheets) is imported client-side only \u2014 on first mount,\n * inside an effect \u2014 which keeps it out of SSR while rendering a prop-sized placeholder until it\n * resolves. A single dynamic `import()` drives both the lazy load and the fallback.\n */\nexport function DealsMapCanvas(props: DealsMapCanvasProps): React.ReactElement {\n const [Impl, setImpl] = React.useState<DealsMapCanvasComponent | null>(null)\n React.useEffect(() => {\n let cancelled = false\n void import('./DealsMapCanvasImpl').then((mod) => {\n if (!cancelled) setImpl(() => mod.default)\n })\n return () => {\n cancelled = true\n }\n }, [])\n\n if (!Impl) {\n return (\n <div className={`flex items-center justify-center bg-muted/30 ${props.className ?? ''}`.trim()}>\n <Spinner className=\"size-6 text-muted-foreground\" />\n </div>\n )\n }\n return <Impl {...props} />\n}\n\nexport default DealsMapCanvas\n"],
|
|
5
|
+
"mappings": ";AAuEQ;AArER,YAAY,WAAW;AACvB,SAAS,eAAe;AAqDjB,SAAS,eAAe,OAAgD;AAC7E,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAyC,IAAI;AAC3E,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,SAAK,OAAO,sBAAsB,EAAE,KAAK,CAAC,QAAQ;AAChD,UAAI,CAAC,UAAW,SAAQ,MAAM,IAAI,OAAO;AAAA,IAC3C,CAAC;AACD,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,MAAI,CAAC,MAAM;AACT,WACE,oBAAC,SAAI,WAAW,gDAAgD,MAAM,aAAa,EAAE,GAAG,KAAK,GAC3F,8BAAC,WAAQ,WAAU,gCAA+B,GACpD;AAAA,EAEJ;AACA,SAAO,oBAAC,QAAM,GAAG,OAAO;AAC1B;AAEA,IAAO,yBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import * as L from "leaflet";
|
|
6
|
+
import "leaflet.markercluster";
|
|
7
|
+
import "leaflet/dist/leaflet.css";
|
|
8
|
+
import "leaflet.markercluster/dist/MarkerCluster.css";
|
|
9
|
+
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
|
10
|
+
import { X } from "lucide-react";
|
|
11
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
12
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
13
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
14
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
15
|
+
import { formatCurrency } from "../../../../../components/detail/utils.js";
|
|
16
|
+
const leafletRuntime = L.default ?? L;
|
|
17
|
+
const TILE_URL = process.env.NEXT_PUBLIC_OM_DEALS_MAP_TILE_URL ?? "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
18
|
+
const TILE_ATTRIBUTION = process.env.NEXT_PUBLIC_OM_DEALS_MAP_TILE_ATTRIBUTION ?? '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
|
19
|
+
const USING_PUBLIC_OSM_TILES = TILE_URL.includes("tile.openstreetmap.org");
|
|
20
|
+
let publicOsmTileWarningEmitted = false;
|
|
21
|
+
function warnOnPublicOsmTilesOnce() {
|
|
22
|
+
if (publicOsmTileWarningEmitted || !USING_PUBLIC_OSM_TILES) return;
|
|
23
|
+
publicOsmTileWarningEmitted = true;
|
|
24
|
+
console.warn(
|
|
25
|
+
"[open-mercato] Deals map is using the public OpenStreetMap tile server. OSM's tile usage policy prohibits production/commercial traffic \u2014 point NEXT_PUBLIC_OM_DEALS_MAP_TILE_URL at a self-hosted or commercial tile service before deploying."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const WORLD_CENTER = [20, 0];
|
|
29
|
+
const WORLD_ZOOM = 2;
|
|
30
|
+
const SINGLE_PIN_ZOOM = 12;
|
|
31
|
+
const FIT_PADDING = [32, 32];
|
|
32
|
+
const PIN_TONE_CLASS = {
|
|
33
|
+
success: "bg-status-success-icon",
|
|
34
|
+
error: "bg-status-error-icon",
|
|
35
|
+
warning: "bg-status-warning-icon",
|
|
36
|
+
info: "bg-status-info-icon",
|
|
37
|
+
neutral: "bg-status-neutral-icon",
|
|
38
|
+
brand: "bg-brand-violet",
|
|
39
|
+
pink: "bg-status-pink-icon"
|
|
40
|
+
};
|
|
41
|
+
const STAGE_BADGE_TONE_CLASS = {
|
|
42
|
+
success: "bg-status-success-bg text-status-success-text",
|
|
43
|
+
error: "bg-status-error-bg text-status-error-text",
|
|
44
|
+
warning: "bg-status-warning-bg text-status-warning-text",
|
|
45
|
+
info: "bg-status-info-bg text-status-info-text",
|
|
46
|
+
neutral: "bg-status-neutral-bg text-status-neutral-text",
|
|
47
|
+
brand: "bg-brand-violet/14 text-brand-violet",
|
|
48
|
+
pink: "bg-status-pink-bg text-status-pink-text"
|
|
49
|
+
};
|
|
50
|
+
function getPinToneClass(tone) {
|
|
51
|
+
if (tone && tone in PIN_TONE_CLASS) return PIN_TONE_CLASS[tone];
|
|
52
|
+
return "bg-status-neutral-icon";
|
|
53
|
+
}
|
|
54
|
+
function getStageBadgeClass(tone) {
|
|
55
|
+
if (tone && tone in STAGE_BADGE_TONE_CLASS) return STAGE_BADGE_TONE_CLASS[tone];
|
|
56
|
+
return "bg-muted text-muted-foreground";
|
|
57
|
+
}
|
|
58
|
+
function buildMarkerIcon(dealId, tone, selected) {
|
|
59
|
+
const sizePx = selected ? 20 : 14;
|
|
60
|
+
const dotClass = selected ? `block size-5 rounded-full border-2 border-card shadow-md ring-2 ring-ring ${getPinToneClass(tone)}` : `block size-3.5 rounded-full border-2 border-card shadow-md ${getPinToneClass(tone)}`;
|
|
61
|
+
return L.divIcon({
|
|
62
|
+
className: `om-deal-map-marker${selected ? " om-deal-map-marker--selected" : ""}`,
|
|
63
|
+
html: `<span class="${dotClass}" data-deal-id="${dealId}"></span>`,
|
|
64
|
+
iconSize: [sizePx, sizePx],
|
|
65
|
+
iconAnchor: [sizePx / 2, sizePx / 2]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function buildClusterIcon(count) {
|
|
69
|
+
return L.divIcon({
|
|
70
|
+
className: "om-deal-map-cluster",
|
|
71
|
+
html: `<span class="flex size-9 items-center justify-center rounded-full border-2 border-card bg-brand-violet text-xs font-bold text-brand-violet-foreground shadow-md">${count}</span>`,
|
|
72
|
+
iconSize: [36, 36],
|
|
73
|
+
iconAnchor: [18, 18]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const shortDateFormatter = new Intl.DateTimeFormat(void 0, {
|
|
77
|
+
month: "short",
|
|
78
|
+
day: "2-digit"
|
|
79
|
+
});
|
|
80
|
+
function formatShortDate(value) {
|
|
81
|
+
if (!value) return null;
|
|
82
|
+
const date = new Date(value);
|
|
83
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
84
|
+
return shortDateFormatter.format(date);
|
|
85
|
+
}
|
|
86
|
+
function DealMapPreviewCard({ preview, onClose }) {
|
|
87
|
+
const t = useT();
|
|
88
|
+
const closeDate = formatShortDate(preview.expectedCloseAt);
|
|
89
|
+
const metaParts = [];
|
|
90
|
+
if (typeof preview.valueAmount === "number") {
|
|
91
|
+
metaParts.push(formatCurrency(preview.valueAmount, preview.valueCurrency));
|
|
92
|
+
}
|
|
93
|
+
if (typeof preview.probability === "number") {
|
|
94
|
+
metaParts.push(
|
|
95
|
+
translateWithFallback(t, "customers.deals.map.preview.probabilityShort", "{value}%", {
|
|
96
|
+
value: Math.min(Math.max(Math.round(preview.probability), 0), 100)
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (closeDate) {
|
|
101
|
+
metaParts.push(
|
|
102
|
+
translateWithFallback(t, "customers.deals.map.preview.closeShort", "Close {date}", {
|
|
103
|
+
date: closeDate
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return /* @__PURE__ */ jsxs(
|
|
108
|
+
"div",
|
|
109
|
+
{
|
|
110
|
+
"data-map-preview-card": preview.id,
|
|
111
|
+
className: "absolute right-3 top-3 z-10 flex w-80 max-w-full flex-col gap-2 rounded-xl border border-border bg-popover p-4 shadow-lg",
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
114
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
115
|
+
preview.companyLabel ? /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium leading-normal text-foreground", children: preview.companyLabel }) : null,
|
|
116
|
+
preview.locationLine ? /* @__PURE__ */ jsx("span", { className: "truncate text-xs leading-normal text-muted-foreground", children: preview.locationLine }) : null
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ jsx(
|
|
119
|
+
IconButton,
|
|
120
|
+
{
|
|
121
|
+
variant: "ghost",
|
|
122
|
+
size: "sm",
|
|
123
|
+
type: "button",
|
|
124
|
+
onClick: onClose,
|
|
125
|
+
"aria-label": translateWithFallback(t, "customers.deals.map.preview.close", "Close preview"),
|
|
126
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
] }),
|
|
130
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold leading-normal text-foreground", children: preview.title }),
|
|
131
|
+
metaParts.length > 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm leading-normal text-muted-foreground", children: metaParts.join(" \xB7 ") }) : null,
|
|
132
|
+
preview.stageLabel || preview.ownerName ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
133
|
+
preview.stageLabel ? /* @__PURE__ */ jsx(
|
|
134
|
+
"span",
|
|
135
|
+
{
|
|
136
|
+
className: `inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold leading-normal ${getStageBadgeClass(preview.stageTone)}`,
|
|
137
|
+
children: preview.stageLabel
|
|
138
|
+
}
|
|
139
|
+
) : null,
|
|
140
|
+
preview.ownerName ? /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-md bg-muted px-2 py-0.5 text-xs font-medium leading-normal text-foreground", children: preview.ownerName }) : null
|
|
141
|
+
] }) : null,
|
|
142
|
+
/* @__PURE__ */ jsx(Button, { asChild: true, className: "mt-1 w-full", children: /* @__PURE__ */ jsx(Link, { href: `/backend/customers/deals/${preview.id}`, children: translateWithFallback(t, "customers.deals.map.preview.openDeal", "Open deal") }) })
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
function DealsMapCanvasImpl({
|
|
148
|
+
deals,
|
|
149
|
+
legendStages,
|
|
150
|
+
preview,
|
|
151
|
+
selectedDealId,
|
|
152
|
+
onSelect,
|
|
153
|
+
onCenterChange,
|
|
154
|
+
className
|
|
155
|
+
}) {
|
|
156
|
+
const t = useT();
|
|
157
|
+
const containerRef = React.useRef(null);
|
|
158
|
+
const mapRef = React.useRef(null);
|
|
159
|
+
const clusterRef = React.useRef(null);
|
|
160
|
+
const markersByIdRef = React.useRef(/* @__PURE__ */ new Map());
|
|
161
|
+
const dealsByIdRef = React.useRef(/* @__PURE__ */ new Map());
|
|
162
|
+
const previousSelectedIdRef = React.useRef(null);
|
|
163
|
+
const lastFitSignatureRef = React.useRef(null);
|
|
164
|
+
const centerChangeTimerRef = React.useRef(null);
|
|
165
|
+
const onSelectRef = React.useRef(onSelect);
|
|
166
|
+
onSelectRef.current = onSelect;
|
|
167
|
+
const onCenterChangeRef = React.useRef(onCenterChange);
|
|
168
|
+
onCenterChangeRef.current = onCenterChange;
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
const node = containerRef.current;
|
|
171
|
+
if (!node || mapRef.current) return void 0;
|
|
172
|
+
warnOnPublicOsmTilesOnce();
|
|
173
|
+
const map = L.map(node, { zoomControl: false, worldCopyJump: true });
|
|
174
|
+
L.control.zoom({ position: "topleft" }).addTo(map);
|
|
175
|
+
L.tileLayer(TILE_URL, { attribution: TILE_ATTRIBUTION, maxZoom: 19 }).addTo(map);
|
|
176
|
+
map.setView(WORLD_CENTER, WORLD_ZOOM);
|
|
177
|
+
const cluster = leafletRuntime.markerClusterGroup({
|
|
178
|
+
showCoverageOnHover: false,
|
|
179
|
+
iconCreateFunction: (markerCluster) => buildClusterIcon(markerCluster.getChildCount())
|
|
180
|
+
});
|
|
181
|
+
map.addLayer(cluster);
|
|
182
|
+
map.on("moveend", () => {
|
|
183
|
+
const center = map.getCenter();
|
|
184
|
+
if (centerChangeTimerRef.current) clearTimeout(centerChangeTimerRef.current);
|
|
185
|
+
centerChangeTimerRef.current = setTimeout(() => {
|
|
186
|
+
onCenterChangeRef.current({ latitude: center.lat, longitude: center.lng });
|
|
187
|
+
}, 250);
|
|
188
|
+
});
|
|
189
|
+
mapRef.current = map;
|
|
190
|
+
clusterRef.current = cluster;
|
|
191
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
192
|
+
map.invalidateSize();
|
|
193
|
+
});
|
|
194
|
+
resizeObserver.observe(node);
|
|
195
|
+
return () => {
|
|
196
|
+
resizeObserver.disconnect();
|
|
197
|
+
if (centerChangeTimerRef.current) {
|
|
198
|
+
clearTimeout(centerChangeTimerRef.current);
|
|
199
|
+
centerChangeTimerRef.current = null;
|
|
200
|
+
}
|
|
201
|
+
map.stop();
|
|
202
|
+
map.remove();
|
|
203
|
+
mapRef.current = null;
|
|
204
|
+
clusterRef.current = null;
|
|
205
|
+
markersByIdRef.current.clear();
|
|
206
|
+
dealsByIdRef.current.clear();
|
|
207
|
+
previousSelectedIdRef.current = null;
|
|
208
|
+
lastFitSignatureRef.current = null;
|
|
209
|
+
};
|
|
210
|
+
}, []);
|
|
211
|
+
const dealsSignature = React.useMemo(() => deals.map((deal) => deal.id).join("|"), [deals]);
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
const map = mapRef.current;
|
|
214
|
+
const cluster = clusterRef.current;
|
|
215
|
+
if (!map || !cluster) return;
|
|
216
|
+
cluster.clearLayers();
|
|
217
|
+
markersByIdRef.current.clear();
|
|
218
|
+
dealsByIdRef.current.clear();
|
|
219
|
+
const markers = [];
|
|
220
|
+
for (const deal of deals) {
|
|
221
|
+
const marker = L.marker([deal.latitude, deal.longitude], {
|
|
222
|
+
icon: buildMarkerIcon(deal.id, deal.tone, false),
|
|
223
|
+
keyboard: false
|
|
224
|
+
});
|
|
225
|
+
marker.on("click", () => onSelectRef.current(deal.id));
|
|
226
|
+
markersByIdRef.current.set(deal.id, marker);
|
|
227
|
+
dealsByIdRef.current.set(deal.id, deal);
|
|
228
|
+
markers.push(marker);
|
|
229
|
+
}
|
|
230
|
+
cluster.addLayers(markers);
|
|
231
|
+
if (lastFitSignatureRef.current !== dealsSignature) {
|
|
232
|
+
lastFitSignatureRef.current = dealsSignature;
|
|
233
|
+
map.stop();
|
|
234
|
+
if (deals.length === 0) {
|
|
235
|
+
map.setView(WORLD_CENTER, WORLD_ZOOM, { animate: false });
|
|
236
|
+
} else if (deals.length === 1) {
|
|
237
|
+
map.setView([deals[0].latitude, deals[0].longitude], SINGLE_PIN_ZOOM, { animate: false });
|
|
238
|
+
} else {
|
|
239
|
+
const bounds = L.latLngBounds(
|
|
240
|
+
deals.map((deal) => [deal.latitude, deal.longitude])
|
|
241
|
+
);
|
|
242
|
+
map.fitBounds(bounds, { padding: FIT_PADDING, animate: false });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}, [deals, dealsSignature]);
|
|
246
|
+
React.useEffect(() => {
|
|
247
|
+
const map = mapRef.current;
|
|
248
|
+
const cluster = clusterRef.current;
|
|
249
|
+
if (!map || !cluster) return;
|
|
250
|
+
const previousId = previousSelectedIdRef.current;
|
|
251
|
+
const selectionChanged = previousId !== selectedDealId;
|
|
252
|
+
if (previousId && previousId !== selectedDealId) {
|
|
253
|
+
const previousMarker = markersByIdRef.current.get(previousId);
|
|
254
|
+
const previousDeal = dealsByIdRef.current.get(previousId);
|
|
255
|
+
if (previousMarker && previousDeal) {
|
|
256
|
+
previousMarker.setIcon(buildMarkerIcon(previousDeal.id, previousDeal.tone, false));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
previousSelectedIdRef.current = selectedDealId;
|
|
260
|
+
if (!selectedDealId) return;
|
|
261
|
+
const marker = markersByIdRef.current.get(selectedDealId);
|
|
262
|
+
const deal = dealsByIdRef.current.get(selectedDealId);
|
|
263
|
+
if (!marker || !deal) return;
|
|
264
|
+
marker.setIcon(buildMarkerIcon(deal.id, deal.tone, true));
|
|
265
|
+
if (!selectionChanged) return;
|
|
266
|
+
map.stop();
|
|
267
|
+
const visibleParent = cluster.getVisibleParent(marker);
|
|
268
|
+
if (visibleParent && visibleParent !== marker) {
|
|
269
|
+
cluster.zoomToShowLayer(marker, () => {
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
map.flyTo(marker.getLatLng(), Math.max(map.getZoom(), SINGLE_PIN_ZOOM));
|
|
273
|
+
}
|
|
274
|
+
}, [selectedDealId, deals]);
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
if (!selectedDealId) return void 0;
|
|
277
|
+
const onKeyDown = (event) => {
|
|
278
|
+
if (event.key !== "Escape") return;
|
|
279
|
+
onSelectRef.current(null);
|
|
280
|
+
};
|
|
281
|
+
document.addEventListener("keydown", onKeyDown);
|
|
282
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
283
|
+
}, [selectedDealId]);
|
|
284
|
+
return /* @__PURE__ */ jsxs(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
role: "region",
|
|
288
|
+
"aria-label": translateWithFallback(t, "customers.deals.map.canvas.label", "Deals map"),
|
|
289
|
+
className: `relative ${className ?? ""}`.trim(),
|
|
290
|
+
children: [
|
|
291
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, "data-map-canvas": true, className: "absolute inset-0 z-0" }),
|
|
292
|
+
deals.length === 0 ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-6", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm rounded-xl border border-border bg-card/95 p-4 text-center shadow-sm", children: [
|
|
293
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium leading-normal text-foreground", children: translateWithFallback(t, "customers.deals.map.panel.empty.title", "No deals on the map yet") }),
|
|
294
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs leading-normal text-muted-foreground", children: translateWithFallback(
|
|
295
|
+
t,
|
|
296
|
+
"customers.deals.map.panel.empty.description",
|
|
297
|
+
"Add latitude and longitude to company or person addresses to plot their deals here."
|
|
298
|
+
) })
|
|
299
|
+
] }) }) : null,
|
|
300
|
+
legendStages.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "absolute bottom-3 left-3 z-10 flex flex-col gap-1.5 rounded-lg border border-border bg-card/95 p-3", children: [
|
|
301
|
+
/* @__PURE__ */ jsx("span", { className: "text-overline font-bold uppercase leading-normal text-muted-foreground", children: translateWithFallback(t, "customers.deals.map.legend.title", "Stages") }),
|
|
302
|
+
legendStages.map((stage) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-xs leading-normal text-foreground", children: [
|
|
303
|
+
/* @__PURE__ */ jsx(
|
|
304
|
+
"span",
|
|
305
|
+
{
|
|
306
|
+
className: `size-2.5 shrink-0 rounded-full ${getPinToneClass(stage.tone)}`,
|
|
307
|
+
"aria-hidden": "true"
|
|
308
|
+
}
|
|
309
|
+
),
|
|
310
|
+
stage.label
|
|
311
|
+
] }, stage.id))
|
|
312
|
+
] }) : null,
|
|
313
|
+
preview ? /* @__PURE__ */ jsx(DealMapPreviewCard, { preview, onClose: () => onSelectRef.current(null) }) : null
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
DealsMapCanvasImpl as default
|
|
320
|
+
};
|
|
321
|
+
//# sourceMappingURL=DealsMapCanvasImpl.js.map
|
package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport * as L from 'leaflet'\nimport 'leaflet.markercluster'\nimport 'leaflet/dist/leaflet.css'\nimport 'leaflet.markercluster/dist/MarkerCluster.css'\nimport 'leaflet.markercluster/dist/MarkerCluster.Default.css'\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'\nimport type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { formatCurrency } from '../../../../../components/detail/utils'\nimport type {\n DealsMapCanvasDeal,\n DealsMapCanvasProps,\n DealsMapPreview,\n} from './DealsMapCanvas'\n\nconst leafletRuntime = ((L as { default?: typeof L }).default ?? L) as typeof L\n\nconst TILE_URL =\n process.env.NEXT_PUBLIC_OM_DEALS_MAP_TILE_URL ?? 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'\nconst TILE_ATTRIBUTION =\n process.env.NEXT_PUBLIC_OM_DEALS_MAP_TILE_ATTRIBUTION ??\n '© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors'\n// True whenever the EFFECTIVE tile URL targets OSM's public CDN \u2014 whether from the bundled default\n// (env unset) OR an env value pointed back at the same public host. OSM's tile usage policy\n// prohibits production/commercial traffic against it.\nconst USING_PUBLIC_OSM_TILES = TILE_URL.includes('tile.openstreetmap.org')\n\nlet publicOsmTileWarningEmitted = false\n// Warn once (per session) so deployments point NEXT_PUBLIC_OM_DEALS_MAP_TILE_URL at a self-hosted or\n// commercial tile service instead of silently shipping against the shared public CDN.\nfunction warnOnPublicOsmTilesOnce(): void {\n if (publicOsmTileWarningEmitted || !USING_PUBLIC_OSM_TILES) return\n publicOsmTileWarningEmitted = true\n // eslint-disable-next-line no-console\n console.warn(\n '[open-mercato] Deals map is using the public OpenStreetMap tile server. ' +\n \"OSM's tile usage policy prohibits production/commercial traffic \u2014 point \" +\n 'NEXT_PUBLIC_OM_DEALS_MAP_TILE_URL at a self-hosted or commercial tile service before deploying.',\n )\n}\n\nconst WORLD_CENTER: L.LatLngTuple = [20, 0]\nconst WORLD_ZOOM = 2\nconst SINGLE_PIN_ZOOM = 12\nconst FIT_PADDING: L.PointTuple = [32, 32]\n\n// Saturated pin fill per stage tone \u2014 mirrors `Lane.tsx` ACCENT_TONE_CLASS so the same\n// stage reads as the same color on the kanban color bar and on the map pin.\nconst PIN_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// Stage badge surface for the preview card \u2014 mirrors `Lane.tsx` COUNT_BADGE_TONE_CLASS.\nconst STAGE_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 getPinToneClass(tone: FilterOptionTone | null): string {\n if (tone && tone in PIN_TONE_CLASS) return PIN_TONE_CLASS[tone]\n return 'bg-status-neutral-icon'\n}\n\nfunction getStageBadgeClass(tone: FilterOptionTone | null): string {\n if (tone && tone in STAGE_BADGE_TONE_CLASS) return STAGE_BADGE_TONE_CLASS[tone]\n return 'bg-muted text-muted-foreground'\n}\n\n// divIcon html is a raw string (no React) \u2014 only the server-issued deal id is interpolated,\n// never user-entered text, and styling comes exclusively from DS token classes.\nfunction buildMarkerIcon(dealId: string, tone: FilterOptionTone | null, selected: boolean): L.DivIcon {\n const sizePx = selected ? 20 : 14\n const dotClass = selected\n ? `block size-5 rounded-full border-2 border-card shadow-md ring-2 ring-ring ${getPinToneClass(tone)}`\n : `block size-3.5 rounded-full border-2 border-card shadow-md ${getPinToneClass(tone)}`\n return L.divIcon({\n className: `om-deal-map-marker${selected ? ' om-deal-map-marker--selected' : ''}`,\n html: `<span class=\"${dotClass}\" data-deal-id=\"${dealId}\"></span>`,\n iconSize: [sizePx, sizePx],\n iconAnchor: [sizePx / 2, sizePx / 2],\n })\n}\n\nfunction buildClusterIcon(count: number): L.DivIcon {\n return L.divIcon({\n className: 'om-deal-map-cluster',\n html: `<span class=\"flex size-9 items-center justify-center rounded-full border-2 border-card bg-brand-violet text-xs font-bold text-brand-violet-foreground shadow-md\">${count}</span>`,\n iconSize: [36, 36],\n iconAnchor: [18, 18],\n })\n}\n\nconst shortDateFormatter = new Intl.DateTimeFormat(undefined, {\n month: 'short',\n day: '2-digit',\n})\n\nfunction formatShortDate(value: string | null): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return shortDateFormatter.format(date)\n}\n\ntype PreviewCardProps = {\n preview: DealsMapPreview\n onClose: () => void\n}\n\nfunction DealMapPreviewCard({ preview, onClose }: PreviewCardProps): React.ReactElement {\n const t = useT()\n const closeDate = formatShortDate(preview.expectedCloseAt)\n const metaParts: string[] = []\n if (typeof preview.valueAmount === 'number') {\n metaParts.push(formatCurrency(preview.valueAmount, preview.valueCurrency))\n }\n if (typeof preview.probability === 'number') {\n metaParts.push(\n translateWithFallback(t, 'customers.deals.map.preview.probabilityShort', '{value}%', {\n value: Math.min(Math.max(Math.round(preview.probability), 0), 100),\n }),\n )\n }\n if (closeDate) {\n metaParts.push(\n translateWithFallback(t, 'customers.deals.map.preview.closeShort', 'Close {date}', {\n date: closeDate,\n }),\n )\n }\n return (\n <div\n data-map-preview-card={preview.id}\n className=\"absolute right-3 top-3 z-10 flex w-80 max-w-full flex-col gap-2 rounded-xl border border-border bg-popover p-4 shadow-lg\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 flex-col\">\n {preview.companyLabel ? (\n <span className=\"truncate text-sm font-medium leading-normal text-foreground\">\n {preview.companyLabel}\n </span>\n ) : null}\n {preview.locationLine ? (\n <span className=\"truncate text-xs leading-normal text-muted-foreground\">\n {preview.locationLine}\n </span>\n ) : null}\n </div>\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={onClose}\n aria-label={translateWithFallback(t, 'customers.deals.map.preview.close', 'Close preview')}\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </IconButton>\n </div>\n <h3 className=\"text-base font-semibold leading-normal text-foreground\">{preview.title}</h3>\n {metaParts.length > 0 ? (\n <p className=\"text-sm leading-normal text-muted-foreground\">{metaParts.join(' \u00B7 ')}</p>\n ) : null}\n {preview.stageLabel || preview.ownerName ? (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {preview.stageLabel ? (\n <span\n className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold leading-normal ${getStageBadgeClass(preview.stageTone)}`}\n >\n {preview.stageLabel}\n </span>\n ) : null}\n {preview.ownerName ? (\n <span className=\"inline-flex items-center rounded-md bg-muted px-2 py-0.5 text-xs font-medium leading-normal text-foreground\">\n {preview.ownerName}\n </span>\n ) : null}\n </div>\n ) : null}\n <Button asChild className=\"mt-1 w-full\">\n <Link href={`/backend/customers/deals/${preview.id}`}>\n {translateWithFallback(t, 'customers.deals.map.preview.openDeal', 'Open deal')}\n </Link>\n </Button>\n </div>\n )\n}\n\nexport default function DealsMapCanvasImpl({\n deals,\n legendStages,\n preview,\n selectedDealId,\n onSelect,\n onCenterChange,\n className,\n}: DealsMapCanvasProps): React.ReactElement {\n const t = useT()\n const containerRef = React.useRef<HTMLDivElement | null>(null)\n const mapRef = React.useRef<L.Map | null>(null)\n const clusterRef = React.useRef<L.MarkerClusterGroup | null>(null)\n const markersByIdRef = React.useRef(new Map<string, L.Marker>())\n const dealsByIdRef = React.useRef(new Map<string, DealsMapCanvasDeal>())\n const previousSelectedIdRef = React.useRef<string | null>(null)\n const lastFitSignatureRef = React.useRef<string | null>(null)\n const centerChangeTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const onSelectRef = React.useRef(onSelect)\n onSelectRef.current = onSelect\n const onCenterChangeRef = React.useRef(onCenterChange)\n onCenterChangeRef.current = onCenterChange\n\n React.useEffect(() => {\n const node = containerRef.current\n if (!node || mapRef.current) return undefined\n warnOnPublicOsmTilesOnce()\n const map = L.map(node, { zoomControl: false, worldCopyJump: true })\n // Zoom sits top-LEFT so it never overlaps the selection preview card (top-right overlay).\n L.control.zoom({ position: 'topleft' }).addTo(map)\n L.tileLayer(TILE_URL, { attribution: TILE_ATTRIBUTION, maxZoom: 19 }).addTo(map)\n map.setView(WORLD_CENTER, WORLD_ZOOM)\n const cluster = leafletRuntime.markerClusterGroup({\n showCoverageOnHover: false,\n iconCreateFunction: (markerCluster) => buildClusterIcon(markerCluster.getChildCount()),\n })\n map.addLayer(cluster)\n // `moveend` fires on every pan/zoom-end and each one re-renders the view + re-runs the panel's\n // proximity (haversine) sort, so debounce the state update. Read the center synchronously here\n // while the map pane is guaranteed positioned \u2014 deferring getCenter() into the timeout can hit\n // Leaflet's `_leaflet_pos` on a pane that has since been reset (fitBounds) or torn down.\n map.on('moveend', () => {\n const center = map.getCenter()\n if (centerChangeTimerRef.current) clearTimeout(centerChangeTimerRef.current)\n centerChangeTimerRef.current = setTimeout(() => {\n onCenterChangeRef.current({ latitude: center.lat, longitude: center.lng })\n }, 250)\n })\n mapRef.current = map\n clusterRef.current = cluster\n const resizeObserver = new ResizeObserver(() => {\n map.invalidateSize()\n })\n resizeObserver.observe(node)\n return () => {\n resizeObserver.disconnect()\n if (centerChangeTimerRef.current) {\n clearTimeout(centerChangeTimerRef.current)\n centerChangeTimerRef.current = null\n }\n // Cancel any in-flight pan/zoom/fly animation before teardown \u2014 otherwise its\n // CSS transitionend fires after the panes are gone and Leaflet reads `_leaflet_pos`\n // off an undefined map pane.\n map.stop()\n map.remove()\n mapRef.current = null\n clusterRef.current = null\n markersByIdRef.current.clear()\n dealsByIdRef.current.clear()\n previousSelectedIdRef.current = null\n lastFitSignatureRef.current = null\n }\n }, [])\n\n const dealsSignature = React.useMemo(() => deals.map((deal) => deal.id).join('|'), [deals])\n\n React.useEffect(() => {\n const map = mapRef.current\n const cluster = clusterRef.current\n if (!map || !cluster) return\n cluster.clearLayers()\n markersByIdRef.current.clear()\n dealsByIdRef.current.clear()\n const markers: L.Marker[] = []\n for (const deal of deals) {\n const marker = L.marker([deal.latitude, deal.longitude], {\n icon: buildMarkerIcon(deal.id, deal.tone, false),\n keyboard: false,\n })\n marker.on('click', () => onSelectRef.current(deal.id))\n markersByIdRef.current.set(deal.id, marker)\n dealsByIdRef.current.set(deal.id, deal)\n markers.push(marker)\n }\n cluster.addLayers(markers)\n if (lastFitSignatureRef.current !== dealsSignature) {\n lastFitSignatureRef.current = dealsSignature\n // Fit instantly (animate: false). The list pages in over several updates, so animated\n // re-fits would stack overlapping zoom transitions and race Leaflet's pane bookkeeping.\n map.stop()\n if (deals.length === 0) {\n map.setView(WORLD_CENTER, WORLD_ZOOM, { animate: false })\n } else if (deals.length === 1) {\n map.setView([deals[0].latitude, deals[0].longitude], SINGLE_PIN_ZOOM, { animate: false })\n } else {\n const bounds = L.latLngBounds(\n deals.map((deal) => [deal.latitude, deal.longitude] as L.LatLngTuple),\n )\n map.fitBounds(bounds, { padding: FIT_PADDING, animate: false })\n }\n }\n }, [deals, dealsSignature])\n\n React.useEffect(() => {\n const map = mapRef.current\n const cluster = clusterRef.current\n if (!map || !cluster) return\n const previousId = previousSelectedIdRef.current\n const selectionChanged = previousId !== selectedDealId\n if (previousId && previousId !== selectedDealId) {\n const previousMarker = markersByIdRef.current.get(previousId)\n const previousDeal = dealsByIdRef.current.get(previousId)\n if (previousMarker && previousDeal) {\n previousMarker.setIcon(buildMarkerIcon(previousDeal.id, previousDeal.tone, false))\n }\n }\n previousSelectedIdRef.current = selectedDealId\n if (!selectedDealId) return\n const marker = markersByIdRef.current.get(selectedDealId)\n const deal = dealsByIdRef.current.get(selectedDealId)\n if (!marker || !deal) return\n marker.setIcon(buildMarkerIcon(deal.id, deal.tone, true))\n // Only move the camera when the selection itself changed. This effect also depends on\n // `deals` so the selected marker's icon is re-applied after a marker rebuild \u2014 but a deal-set\n // change alone (e.g. a sibling query resolving) must not yank the viewport back to the pin.\n if (!selectionChanged) return\n map.stop()\n const visibleParent = cluster.getVisibleParent(marker)\n if (visibleParent && visibleParent !== marker) {\n cluster.zoomToShowLayer(marker, () => {})\n } else {\n map.flyTo(marker.getLatLng(), Math.max(map.getZoom(), SINGLE_PIN_ZOOM))\n }\n }, [selectedDealId, deals])\n\n // Escape clears the selected-deal preview. Bound at the document level (not the canvas region's\n // onKeyDown) so it fires regardless of whether focus is on the map, a panel card, or the preview\n // card. Registered only while a deal is selected, so it never swallows Escape elsewhere.\n React.useEffect(() => {\n if (!selectedDealId) return undefined\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return\n onSelectRef.current(null)\n }\n document.addEventListener('keydown', onKeyDown)\n return () => document.removeEventListener('keydown', onKeyDown)\n }, [selectedDealId])\n\n return (\n <div\n role=\"region\"\n aria-label={translateWithFallback(t, 'customers.deals.map.canvas.label', 'Deals map')}\n className={`relative ${className ?? ''}`.trim()}\n >\n <div ref={containerRef} data-map-canvas className=\"absolute inset-0 z-0\" />\n {deals.length === 0 ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-6\">\n <div className=\"max-w-sm rounded-xl border border-border bg-card/95 p-4 text-center shadow-sm\">\n <p className=\"text-sm font-medium leading-normal text-foreground\">\n {translateWithFallback(t, 'customers.deals.map.panel.empty.title', 'No deals on the map yet')}\n </p>\n <p className=\"mt-1 text-xs leading-normal text-muted-foreground\">\n {translateWithFallback(\n t,\n 'customers.deals.map.panel.empty.description',\n 'Add latitude and longitude to company or person addresses to plot their deals here.',\n )}\n </p>\n </div>\n </div>\n ) : null}\n {legendStages.length > 0 ? (\n <div className=\"absolute bottom-3 left-3 z-10 flex flex-col gap-1.5 rounded-lg border border-border bg-card/95 p-3\">\n <span className=\"text-overline font-bold uppercase leading-normal text-muted-foreground\">\n {translateWithFallback(t, 'customers.deals.map.legend.title', 'Stages')}\n </span>\n {legendStages.map((stage) => (\n <span key={stage.id} className=\"flex items-center gap-2 text-xs leading-normal text-foreground\">\n <span\n className={`size-2.5 shrink-0 rounded-full ${getPinToneClass(stage.tone)}`}\n aria-hidden=\"true\"\n />\n {stage.label}\n </span>\n ))}\n </div>\n ) : null}\n {preview ? <DealMapPreviewCard preview={preview} onClose={() => onSelectRef.current(null)} /> : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|