@open-mercato/core 0.6.3-develop.3651.1.d3a548a5f1 → 0.6.3-develop.3653.1.b8558737ae
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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useDraggable } from '@dnd-kit/core'\nimport { AlertTriangle, Building2, Calendar, Clock, Mail, Phone, StickyNote } from 'lucide-react'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { DealCardMenu } from './DealCardMenu'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\n\nexport type DealCardPipelineState = {\n openActivitiesCount: number\n daysInCurrentStage: number\n isStuck: boolean\n isOverdue: boolean\n}\n\nexport type DealCardAssociation = {\n id: string\n label: string\n}\n\nexport type DealCardOwner = {\n userId: string\n label: string\n}\n\nexport type DealCardData = {\n id: string\n title: string\n status: string | null\n valueAmount: number | null\n valueCurrency: string | null\n probability: number | null\n expectedCloseAt: string | null\n createdAt: string | null\n /** ISO timestamp of last update \u2014 drives the \"Updated (newest/oldest)\" client-side sort fallback. */\n updatedAt: string | null\n owner: DealCardOwner | null\n primaryCompany: DealCardAssociation | null\n pipelineState: DealCardPipelineState\n}\n\ntype DealCardProps = {\n deal: DealCardData\n selected: boolean\n /**\n * `true` whenever at least one deal anywhere on the board is selected. Forces the\n * checkbox to render even when this card is not yet selected and not currently hovered,\n * so the operator can multi-select without re-aiming at the small 16px target after\n * every click. Without this, the second-click had to land precisely on the still-hidden\n * checkbox area before the `:hover` made it visible, which is what the spec called\n * \"persistent visibility once any card is selected\".\n */\n bulkSelectionActive?: boolean\n /** Stable function called once per render to build the row-action items for this deal */\n buildMenuItems: (deal: DealCardData) => RowActionItem[]\n extraCompaniesCount?: number\n extraOwners?: DealCardOwner[]\n isActiveDrag?: boolean\n onToggleSelect: (dealId: string) => void\n onComposeActivity: (dealId: string, type: 'call' | 'email' | 'note') => void\n onOpenDetail: (dealId: string) => void\n}\n\nconst ACTIVITY_BADGE_CAP = 9\nconst ACTIVITY_BADGE_WARNING_THRESHOLD = 9\nconst AVATAR_STACK_MAX = 3\n\n// Hash-derived avatar accent classes that map to semantic DS tokens (no hardcoded oklch/hex).\n// Each entry uses the saturated `*-icon` background (Figma calls for vivid, not pale, avatars)\n// paired with white-on-tone text since `*-icon` tokens are saturated foreground colors. The\n// choice is deterministic-but-arbitrary \u2014 we only need stable visual differentiation between\n// different owners, not semantic mapping to status meanings.\nconst AVATAR_ACCENT_CLASSES: string[] = [\n 'bg-status-info-icon text-white',\n 'bg-brand-violet text-brand-violet-foreground',\n 'bg-status-warning-icon text-white',\n 'bg-status-neutral-icon text-white',\n 'bg-status-success-icon text-white',\n 'bg-status-pink-icon text-white',\n]\n\nfunction hashAccent(seed: string): string {\n // Deterministic hash (Java string-hash variant). The bit pattern is not cryptographically\n // meaningful \u2014 it only needs to spread inputs across the palette indices.\n let h = 0\n for (let i = 0; i < seed.length; i += 1) {\n h = ((h << 5) - h + seed.charCodeAt(i)) | 0\n }\n const idx = Math.abs(h) % AVATAR_ACCENT_CLASSES.length\n return AVATAR_ACCENT_CLASSES[idx]\n}\n\nfunction splitCurrencyAmount(amount: number, currency: string | null): { display: string; code: string | null } {\n const code = currency && currency.length === 3 ? currency.toUpperCase() : null\n try {\n const formatter = new Intl.NumberFormat(undefined, {\n style: 'decimal',\n maximumFractionDigits: 0,\n useGrouping: true,\n })\n return { display: formatter.format(amount), code }\n } catch {\n return { display: String(Math.round(amount)), code }\n }\n}\n\nfunction formatProbability(value: number | null): string | null {\n if (typeof value !== 'number' || Number.isNaN(value)) return null\n return `${Math.min(Math.max(Math.round(value), 0), 100)}%`\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\nfunction shortDealRef(id: string): string {\n return `DEAL-${id.slice(0, 6).toUpperCase()}`\n}\n\nfunction DealCardImpl({\n deal,\n selected,\n bulkSelectionActive = false,\n buildMenuItems,\n extraCompaniesCount = 0,\n extraOwners,\n isActiveDrag = false,\n onToggleSelect,\n onComposeActivity,\n onOpenDetail,\n}: DealCardProps): React.ReactElement {\n const menuItems = React.useMemo(() => buildMenuItems(deal), [buildMenuItems, deal])\n const t = useT()\n // useDraggable is significantly cheaper than useSortable: dnd-kit doesn't recompute sibling\n // transforms during a drag. We don't reorder within a lane (sortBy controls order), so we just\n // need a draggable handle that drops into a Lane droppable.\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: deal.id,\n data: { type: 'deal' },\n })\n const dimmed = isActiveDrag || isDragging\n\n // Note: no transform CSS \u2014 the DragOverlay shows the moving copy, while the source card stays\n // visually in place (just dimmed). This removes per-frame transform recomputation.\n const style: React.CSSProperties = {}\n\n const activityCount = deal.pipelineState.openActivitiesCount\n const activityBadgeLabel =\n activityCount > 0\n ? activityCount > ACTIVITY_BADGE_CAP\n ? `${ACTIVITY_BADGE_CAP}+`\n : String(activityCount)\n : null\n const activityWarning = activityCount >= ACTIVITY_BADGE_WARNING_THRESHOLD\n\n const probabilityLabel = formatProbability(deal.probability)\n const dateLabel = formatShortDate(deal.expectedCloseAt ?? deal.createdAt)\n const stageDaysLabel = deal.pipelineState.isOverdue\n ? translateWithFallback(t, 'customers.deals.kanban.card.overdueLabel', 'Overdue')\n : translateWithFallback(t, 'customers.deals.kanban.card.daysInStage', 'in {days}d', {\n days: deal.pipelineState.daysInCurrentStage,\n })\n\n const showOverdue = deal.pipelineState.isOverdue\n const showStuck = !showOverdue && deal.pipelineState.isStuck\n\n const handleSelectChange = (next: boolean | 'indeterminate') => {\n if (typeof next === 'boolean' && next !== selected) {\n onToggleSelect(deal.id)\n }\n }\n\n const handleCardClick = (event: React.MouseEvent<HTMLDivElement>) => {\n const target = event.target as HTMLElement\n if (target.closest('[data-card-action=\"true\"]')) return\n // Once at least one card is selected the board is in \"bulk-select mode\" \u2014 clicking\n // anywhere on a card toggles that card's selection rather than navigating to the\n // detail page. This matches Gmail/Asana selection semantics and rescues the\n // user-reported failure mode where a slight mis-aim at the 16px checkbox button\n // navigated to the deal instead of adding it to the selection.\n if (bulkSelectionActive) {\n onToggleSelect(deal.id)\n return\n }\n onOpenDetail(deal.id)\n }\n\n const handleActionClick = (type: 'call' | 'email' | 'note') => (event: React.MouseEvent) => {\n event.stopPropagation()\n onComposeActivity(deal.id, type)\n }\n\n // dnd-kit's `useDraggable` listeners set pointer capture on the card when a pointerdown lands\n // anywhere inside it. With capture active, the subsequent `click` event's `target` is the card\n // itself rather than the inner button/checkbox the user actually clicked \u2014 which made\n // `handleCardClick`'s `closest('[data-card-action=\"true\"]')` check return null, so every click\n // inside Select / Call / Email / Note / kebab navigated to the deal detail instead of running\n // the intended handler.\n //\n // We stop propagation of `onPointerDown` at the `data-card-action=\"true\"` boundary so dnd-kit's\n // listener never sees the pointerdown and never sets capture. React's `onClick` handlers on the\n // children then run normally with the right `event.target`.\n const stopPointerDown = React.useCallback((event: React.PointerEvent) => {\n event.stopPropagation()\n }, [])\n\n const ariaLabel = translateWithFallback(t, 'customers.deals.kanban.card.aria', 'Deal: {title}', {\n title: deal.title,\n })\n const ariaRoleDescription = translateWithFallback(\n t,\n 'customers.deals.kanban.card.aria.roledescription',\n 'deal card',\n )\n\n const valuePieces =\n deal.valueAmount !== null && Number.isFinite(deal.valueAmount)\n ? splitCurrencyAmount(deal.valueAmount, deal.valueCurrency)\n : null\n\n // Probability pill tone tracks the probability VALUE, not stage state. Figma uses three\n // tiers (nodes 982:369/417/674/745/797/928/1002):\n // <40% \u2192 de-emphasized (gray bg + faded text) \u2014 Figma `bg-[#f4f5f7] text-[#808794]`\n // 40-69% \u2192 warning tone (orange bg + warning text) \u2014 Figma `bg-[#fff2e0] text-[#fe9a00]`\n // \u226570% \u2192 emphasized (gray bg + dark text) \u2014 Figma `bg-[#f4f5f7] text-[#474c5a]`\n //\n // Importantly the high-probability tier is NOT green: Figma keeps the neutral surface and\n // signals confidence with darker foreground text instead. Stuck/overdue state is already\n // surfaced by the separate badge above, so coloring this pill by stage state (the previous\n // behavior) double-stacked the signal and made e.g. \"80% on a red background\" read as\n // \"80% is bad\" \u2014 the inverse of intent.\n const probabilityValue = typeof deal.probability === 'number' ? deal.probability : null\n const probabilityPillClass =\n probabilityValue !== null && probabilityValue >= 70\n ? 'bg-muted text-foreground'\n : probabilityValue !== null && probabilityValue >= 40\n ? 'bg-status-warning-bg text-status-warning-text'\n : 'bg-muted text-muted-foreground'\n\n const activityBadgeClass = activityWarning\n ? 'bg-status-warning-bg text-status-warning-text'\n : 'bg-muted text-foreground'\n\n const ownersStack: DealCardOwner[] = React.useMemo(() => {\n if (!deal.owner) return extraOwners ?? []\n return [deal.owner, ...(extraOwners ?? [])]\n }, [deal.owner, extraOwners])\n\n const visibleOwners = ownersStack.slice(0, AVATAR_STACK_MAX)\n const ownerOverflow = ownersStack.length - visibleOwners.length\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n {...attributes}\n {...listeners}\n aria-label={ariaLabel}\n aria-roledescription={ariaRoleDescription}\n onClick={handleCardClick}\n className={`group relative flex w-full flex-col gap-3 rounded-lg border border-border bg-card px-4 py-3.5 shadow-xs transition-shadow ${\n dimmed\n ? 'cursor-grabbing opacity-30'\n : bulkSelectionActive\n ? 'cursor-pointer hover:shadow-sm'\n : 'cursor-grab hover:shadow-sm active:cursor-grabbing'\n } ${selected ? 'ring-2 ring-accent-indigo' : ''}`}\n >\n <div className=\"flex items-start justify-between gap-2.5\">\n <div\n data-card-action=\"true\"\n className={`mr-0.5 mt-0.5 flex shrink-0 transition-opacity ${\n selected || bulkSelectionActive\n ? 'opacity-100'\n // `opacity-0` alone leaves the element in the DOM with hit-testable pointer\n // areas \u2014 its `onPointerDown={stopPointerDown}` would then swallow drag\n // starts whenever the operator grabs the card near the (invisible) checkbox,\n // and dnd-kit never sees the gesture. Pairing with `pointer-events-none`\n // lets the drag pass through while preserving tab/focus reachability (focus\n // events ignore `pointer-events`), and the matching `group-hover` /\n // `focus-within` variants restore hit testing the moment the affordance\n // becomes visible. `[@media(hover:none)]` keeps it always-on for touch.\n : 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto [@media(hover:none)]:opacity-100 [@media(hover:none)]:pointer-events-auto'\n }`}\n onClick={(event) => event.stopPropagation()}\n onPointerDown={stopPointerDown}\n >\n <Checkbox\n checked={selected}\n onCheckedChange={handleSelectChange}\n aria-label={translateWithFallback(t, 'customers.deals.kanban.card.aria.select', 'Select deal')}\n />\n </div>\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <h3 className=\"line-clamp-2 text-base font-semibold leading-normal text-foreground\">\n {deal.title}\n </h3>\n <span className=\"text-xs leading-normal text-muted-foreground\">{shortDealRef(deal.id)}</span>\n </div>\n <div\n className=\"flex shrink-0 items-center gap-2\"\n data-card-action=\"true\"\n onClick={(event) => event.stopPropagation()}\n onPointerDown={stopPointerDown}\n >\n {activityBadgeLabel ? (\n <span\n className={`inline-flex items-center justify-center rounded-full px-2 py-px text-xs font-bold leading-normal ${activityBadgeClass}`}\n aria-label={translateWithFallback(\n t,\n 'customers.deals.kanban.card.aria.openActivities',\n '{count} open activities',\n { count: activityCount },\n )}\n >\n {activityBadgeLabel}\n </span>\n ) : null}\n <DealCardMenu\n items={menuItems}\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.card.aria.menu',\n 'Deal actions',\n )}\n />\n </div>\n </div>\n\n {showOverdue || showStuck ? (\n <div>\n {showOverdue ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-status-error-bg px-2.5 py-1 text-xs font-semibold leading-normal text-status-error-text\">\n <AlertTriangle className=\"size-3.5\" aria-hidden=\"true\" />\n {translateWithFallback(t, 'customers.deals.kanban.card.statusOverdue', 'OVERDUE')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-status-warning-bg px-2.5 py-1 text-xs font-semibold leading-normal text-status-warning-text\">\n <Clock className=\"size-3.5\" aria-hidden=\"true\" />\n {translateWithFallback(t, 'customers.deals.kanban.card.statusStuck', 'STUCK')}\n </span>\n )}\n </div>\n ) : null}\n\n {/*\n Quick-log activity actions. `customers.activities` are scoped to a parent entity\n (person/company), so without a primary company on this deal the activity composer\n has nowhere to anchor a new record. The page-level handler previously flashed a\n toast and bailed silently, which was confusing \u2014 we surface the same constraint\n here as a disabled-with-tooltip state so the operator can fix the underlying data.\n */}\n <div\n data-card-action=\"true\"\n // See note on the checkbox wrapper above \u2014 pairing `opacity-0` with `pointer-events-none`\n // is what restores drag-and-drop. Without it the (invisible) Call/Email/Note row sits in\n // the middle of the card and intercepts every pointer-down, so dnd-kit never gets the\n // gesture and the card snaps back to the source lane.\n className=\"flex items-center gap-1 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto [@media(hover:none)]:opacity-100 [@media(hover:none)]:pointer-events-auto\"\n onClick={(event) => event.stopPropagation()}\n onPointerDown={stopPointerDown}\n >\n {(() => {\n const disabled = !deal.primaryCompany\n const disabledTitle = disabled\n ? translateWithFallback(\n t,\n 'customers.deals.kanban.card.action.disabledNoCompany',\n 'Link a company to this deal before logging activities.',\n )\n : undefined\n const baseClass =\n 'inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md px-2 py-1 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'\n return (\n <>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={handleActionClick('call')}\n disabled={disabled}\n title={disabledTitle}\n aria-disabled={disabled || undefined}\n className={baseClass}\n >\n <Phone className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>{translateWithFallback(t, 'customers.deals.kanban.card.action.call', 'Call')}</span>\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={handleActionClick('email')}\n disabled={disabled}\n title={disabledTitle}\n aria-disabled={disabled || undefined}\n className={baseClass}\n >\n <Mail className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>{translateWithFallback(t, 'customers.deals.kanban.card.action.email', 'Email')}</span>\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={handleActionClick('note')}\n disabled={disabled}\n title={disabledTitle}\n aria-disabled={disabled || undefined}\n className={baseClass}\n >\n <StickyNote className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>{translateWithFallback(t, 'customers.deals.kanban.card.action.note', 'Note')}</span>\n </Button>\n </>\n )\n })()}\n </div>\n\n <div className=\"flex items-center justify-between gap-2.5\">\n {valuePieces ? (\n <div className=\"flex items-baseline gap-1.5\">\n <span className=\"text-lg font-bold leading-normal text-foreground\">\n {valuePieces.display}\n </span>\n {valuePieces.code ? (\n <span className=\"text-sm font-semibold leading-normal text-muted-foreground\">\n {valuePieces.code}\n </span>\n ) : null}\n </div>\n ) : (\n <span className=\"text-sm text-muted-foreground\">\n {translateWithFallback(t, 'customers.deals.kanban.card.noValue', 'No value')}\n </span>\n )}\n {probabilityLabel ? (\n <span\n className={`inline-flex items-center rounded-md px-2.5 py-1 text-sm font-semibold leading-normal ${probabilityPillClass}`}\n aria-label={translateWithFallback(\n t,\n 'customers.deals.kanban.card.aria.probability',\n 'Probability: {value}',\n { value: probabilityLabel },\n )}\n >\n {probabilityLabel}\n </span>\n ) : null}\n </div>\n\n {deal.primaryCompany || extraCompaniesCount > 0 ? (\n <div className=\"flex items-center gap-1.5\">\n {deal.primaryCompany ? (\n <span className=\"inline-flex max-w-full items-center gap-1.5 overflow-hidden rounded-md bg-muted px-2.5 py-1 text-sm font-semibold leading-normal text-foreground\">\n <Building2 className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"truncate\">{deal.primaryCompany.label}</span>\n </span>\n ) : null}\n {extraCompaniesCount > 0 ? (\n <span\n className=\"inline-flex shrink-0 items-center rounded-md bg-muted px-2.5 py-1 text-sm font-semibold leading-normal text-muted-foreground\"\n aria-label={translateWithFallback(\n t,\n 'customers.deals.kanban.card.aria.moreCompanies',\n '{count} more companies',\n { count: extraCompaniesCount },\n )}\n >\n +{extraCompaniesCount}\n </span>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"h-px w-full bg-border\" aria-hidden=\"true\" />\n\n <div className=\"flex items-center justify-between gap-2.5\">\n <div className=\"flex items-center gap-2 text-sm leading-normal\">\n {dateLabel ? (\n <span className=\"inline-flex items-center gap-2\">\n <Calendar\n className={`size-3.5 ${showOverdue ? 'text-status-error-icon' : 'text-muted-foreground'}`}\n aria-hidden=\"true\"\n />\n <span\n className={\n showOverdue\n ? 'font-semibold text-status-error-text'\n : 'font-semibold text-foreground'\n }\n >\n {dateLabel}\n </span>\n </span>\n ) : null}\n <span\n className={\n showOverdue\n ? 'font-semibold text-status-error-text'\n : 'font-normal text-muted-foreground'\n }\n >\n {stageDaysLabel}\n </span>\n </div>\n {ownersStack.length > 0 ? (\n <div className=\"flex items-center\">\n {visibleOwners.map((owner, idx) => {\n const accentClass = hashAccent(owner.userId)\n return (\n <Avatar\n key={`${owner.userId}-${idx}`}\n label={owner.label || owner.userId.slice(0, 2).toUpperCase()}\n size=\"sm\"\n className={`size-7 text-xs font-bold ring-2 ring-card ${accentClass} ${idx > 0 ? '-ml-2.5' : ''}`}\n />\n )\n })}\n {ownerOverflow > 0 ? (\n <Avatar\n label={`+${ownerOverflow}`}\n size=\"sm\"\n variant=\"monochrome\"\n className=\"size-7 -ml-2.5 text-xs font-bold ring-2 ring-card\"\n />\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n// Memoize so cards in lanes far from the drag don't re-render on drag-state changes.\n// Custom equality keeps the comparison cheap (we only re-render when something visible changes).\nexport const DealCard = React.memo(DealCardImpl, (prev, next) => {\n if (prev.deal !== next.deal) return false\n if (prev.selected !== next.selected) return false\n if (prev.bulkSelectionActive !== next.bulkSelectionActive) return false\n if (prev.isActiveDrag !== next.isActiveDrag) return false\n if (prev.buildMenuItems !== next.buildMenuItems) return false\n if (prev.extraCompaniesCount !== next.extraCompaniesCount) return false\n if (prev.extraOwners !== next.extraOwners) return false\n // Callback identity changes do NOT trigger re-renders \u2014 they're stable handlers from the page\n return true\n})\n\nexport default DealCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { MoreVertical } from "lucide-react";
|
|
6
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
8
|
+
const MENU_GAP = 6;
|
|
9
|
+
function MenuShortcut({ children }) {
|
|
10
|
+
return /* @__PURE__ */ jsx(
|
|
11
|
+
"kbd",
|
|
12
|
+
{
|
|
13
|
+
"aria-hidden": "true",
|
|
14
|
+
className: "ml-auto inline-flex items-center rounded border border-border bg-muted px-1.5 py-px font-mono text-[10px] font-medium leading-none text-muted-foreground",
|
|
15
|
+
children
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const ITEM_SHORTCUTS = {
|
|
20
|
+
open: "\u21B5",
|
|
21
|
+
edit: "E",
|
|
22
|
+
duplicate: "\u2318D"
|
|
23
|
+
};
|
|
24
|
+
function matchesShortcut(itemId, event) {
|
|
25
|
+
if (itemId === "open" && event.key === "Enter") return true;
|
|
26
|
+
if (itemId === "edit" && (event.key === "e" || event.key === "E") && !event.metaKey && !event.ctrlKey) return true;
|
|
27
|
+
if (itemId === "duplicate" && (event.key === "d" || event.key === "D") && (event.metaKey || event.ctrlKey)) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function DealCardMenu({ items, ariaLabel }) {
|
|
31
|
+
const [open, setOpen] = React.useState(false);
|
|
32
|
+
const [anchor, setAnchor] = React.useState(null);
|
|
33
|
+
const [direction, setDirection] = React.useState("down");
|
|
34
|
+
const btnRef = React.useRef(null);
|
|
35
|
+
const menuRef = React.useRef(null);
|
|
36
|
+
const updatePosition = React.useCallback(() => {
|
|
37
|
+
if (!btnRef.current) return;
|
|
38
|
+
const rect = btnRef.current.getBoundingClientRect();
|
|
39
|
+
setAnchor(rect);
|
|
40
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
41
|
+
const spaceAbove = rect.top;
|
|
42
|
+
setDirection(spaceBelow < 220 && spaceAbove > spaceBelow ? "up" : "down");
|
|
43
|
+
}, []);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (!open) return;
|
|
46
|
+
updatePosition();
|
|
47
|
+
requestAnimationFrame(() => {
|
|
48
|
+
const first = menuRef.current?.querySelector('[role="menuitem"]');
|
|
49
|
+
first?.focus();
|
|
50
|
+
});
|
|
51
|
+
function onDocClick(e) {
|
|
52
|
+
const target = e.target;
|
|
53
|
+
if (menuRef.current && !menuRef.current.contains(target) && btnRef.current && !btnRef.current.contains(target)) {
|
|
54
|
+
setOpen(false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function onKey(e) {
|
|
58
|
+
if (e.key === "Escape") {
|
|
59
|
+
setOpen(false);
|
|
60
|
+
btnRef.current?.focus();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const active = document.activeElement;
|
|
64
|
+
if (active) {
|
|
65
|
+
const tag = active.tagName;
|
|
66
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || active.isContentEditable) return;
|
|
67
|
+
}
|
|
68
|
+
if (!menuRef.current?.contains(active)) return;
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
if (item.id && matchesShortcut(item.id, e)) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
setOpen(false);
|
|
73
|
+
item.onSelect?.();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function onScrollOrResize() {
|
|
79
|
+
updatePosition();
|
|
80
|
+
}
|
|
81
|
+
document.addEventListener("mousedown", onDocClick);
|
|
82
|
+
document.addEventListener("keydown", onKey);
|
|
83
|
+
window.addEventListener("scroll", onScrollOrResize, true);
|
|
84
|
+
window.addEventListener("resize", onScrollOrResize);
|
|
85
|
+
return () => {
|
|
86
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
87
|
+
document.removeEventListener("keydown", onKey);
|
|
88
|
+
window.removeEventListener("scroll", onScrollOrResize, true);
|
|
89
|
+
window.removeEventListener("resize", onScrollOrResize);
|
|
90
|
+
};
|
|
91
|
+
}, [open, updatePosition]);
|
|
92
|
+
const handleMenuKeyDown = React.useCallback((event) => {
|
|
93
|
+
const items2 = Array.from(
|
|
94
|
+
menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []
|
|
95
|
+
);
|
|
96
|
+
if (items2.length === 0) return;
|
|
97
|
+
const currentIdx = items2.findIndex((el) => el === document.activeElement);
|
|
98
|
+
if (event.key === "ArrowDown") {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
const next = items2[(currentIdx + 1 + items2.length) % items2.length];
|
|
101
|
+
next?.focus();
|
|
102
|
+
} else if (event.key === "ArrowUp") {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
const prev = items2[(currentIdx - 1 + items2.length) % items2.length];
|
|
105
|
+
prev?.focus();
|
|
106
|
+
} else if (event.key === "Home") {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
items2[0]?.focus();
|
|
109
|
+
} else if (event.key === "End") {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
items2[items2.length - 1]?.focus();
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
114
|
+
if (!items.length) return null;
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative inline-flex", "data-card-action": "true", onClick: (event) => event.stopPropagation(), children: [
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
IconButton,
|
|
118
|
+
{
|
|
119
|
+
ref: btnRef,
|
|
120
|
+
variant: "ghost",
|
|
121
|
+
size: "xs",
|
|
122
|
+
"aria-haspopup": "menu",
|
|
123
|
+
"aria-expanded": open,
|
|
124
|
+
"aria-label": ariaLabel,
|
|
125
|
+
onClick: (event) => {
|
|
126
|
+
event.stopPropagation();
|
|
127
|
+
setOpen((previous) => !previous);
|
|
128
|
+
requestAnimationFrame(updatePosition);
|
|
129
|
+
},
|
|
130
|
+
className: "inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
131
|
+
children: /* @__PURE__ */ jsx(MoreVertical, { className: "size-3.5", "aria-hidden": "true" })
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
open && anchor && typeof document !== "undefined" ? createPortal(
|
|
135
|
+
/* @__PURE__ */ jsx(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
ref: menuRef,
|
|
139
|
+
role: "menu",
|
|
140
|
+
onKeyDown: handleMenuKeyDown,
|
|
141
|
+
className: "fixed z-dropdown w-52 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-background p-1.5 shadow-md",
|
|
142
|
+
style: {
|
|
143
|
+
top: direction === "down" ? anchor.bottom + MENU_GAP : anchor.top - MENU_GAP,
|
|
144
|
+
left: Math.min(anchor.right, window.innerWidth - 8),
|
|
145
|
+
transform: `translate(-100%, ${direction === "down" ? "0" : "-100%"})`
|
|
146
|
+
},
|
|
147
|
+
children: items.map((item, idx) => {
|
|
148
|
+
const shortcut = item.id ? ITEM_SHORTCUTS[item.id] : null;
|
|
149
|
+
return /* @__PURE__ */ jsxs(
|
|
150
|
+
Button,
|
|
151
|
+
{
|
|
152
|
+
variant: item.destructive ? "destructive-ghost" : "ghost",
|
|
153
|
+
size: "sm",
|
|
154
|
+
type: "button",
|
|
155
|
+
role: "menuitem",
|
|
156
|
+
onClick: (event) => {
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
setOpen(false);
|
|
159
|
+
item.onSelect?.();
|
|
160
|
+
},
|
|
161
|
+
className: `flex w-full items-center justify-start rounded-sm px-3 py-2 text-left text-sm leading-5 transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none ${item.destructive ? "text-status-error-text hover:bg-status-error-bg/30" : "text-foreground"}`,
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label }),
|
|
164
|
+
shortcut ? /* @__PURE__ */ jsx(MenuShortcut, { children: shortcut }) : null
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
item.id ?? `${item.label}-${idx}`
|
|
168
|
+
);
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
),
|
|
172
|
+
document.body
|
|
173
|
+
) : null
|
|
174
|
+
] });
|
|
175
|
+
}
|
|
176
|
+
var DealCardMenu_default = DealCardMenu;
|
|
177
|
+
export {
|
|
178
|
+
DealCardMenu,
|
|
179
|
+
DealCardMenu_default as default
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=DealCardMenu.js.map
|
package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { MoreVertical } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\n\ntype DealCardMenuProps = {\n items: RowActionItem[]\n ariaLabel: string\n}\n\nconst MENU_GAP = 6\n\n// Right-aligned shortcut hint inside a menu row. Mirrors the Figma deal-card menu\n// (node 1045:12254), which shows \u23CE for Open, E for Edit, \u2318D for Duplicate.\n// `aria-hidden=\"true\"` keeps the kbd out of the menu item's accessible name \u2014\n// without this the menu item reads as \"Open deal \u21B5\" instead of \"Open deal\" and\n// `getByRole('menuitem', { name: 'Open deal', exact: true })` fails in tests.\nfunction MenuShortcut({ children }: { children: React.ReactNode }): React.ReactElement {\n return (\n <kbd\n aria-hidden=\"true\"\n className=\"ml-auto inline-flex items-center rounded border border-border bg-muted px-1.5 py-px font-mono text-[10px] font-medium leading-none text-muted-foreground\"\n >\n {children}\n </kbd>\n )\n}\n\n// Map of menu-item id \u2192 keyboard shortcut hint. Stay in sync with `buildMenuItems` in\n// `pipeline/page.tsx` and with `matchesShortcut` below.\nconst ITEM_SHORTCUTS: Record<string, React.ReactNode> = {\n open: '\u21B5',\n edit: 'E',\n duplicate: '\u2318D',\n}\n\nfunction matchesShortcut(itemId: string, event: KeyboardEvent): boolean {\n if (itemId === 'open' && event.key === 'Enter') return true\n if (itemId === 'edit' && (event.key === 'e' || event.key === 'E') && !event.metaKey && !event.ctrlKey) return true\n if (itemId === 'duplicate' && (event.key === 'd' || event.key === 'D') && (event.metaKey || event.ctrlKey)) return true\n return false\n}\n\nexport function DealCardMenu({ items, ariaLabel }: DealCardMenuProps): React.ReactElement | null {\n const [open, setOpen] = React.useState(false)\n const [anchor, setAnchor] = React.useState<DOMRect | null>(null)\n const [direction, setDirection] = React.useState<'down' | 'up'>('down')\n const btnRef = React.useRef<HTMLButtonElement>(null)\n const menuRef = React.useRef<HTMLDivElement>(null)\n\n const updatePosition = React.useCallback(() => {\n if (!btnRef.current) return\n const rect = btnRef.current.getBoundingClientRect()\n setAnchor(rect)\n const spaceBelow = window.innerHeight - rect.bottom\n const spaceAbove = rect.top\n setDirection(spaceBelow < 220 && spaceAbove > spaceBelow ? 'up' : 'down')\n }, [])\n\n React.useEffect(() => {\n if (!open) return\n updatePosition()\n // Autofocus the first menu item once the portal has painted. We schedule this on the\n // next frame so the `createPortal` render has actually mounted the buttons in the DOM.\n requestAnimationFrame(() => {\n const first = menuRef.current?.querySelector<HTMLElement>('[role=\"menuitem\"]')\n first?.focus()\n })\n function onDocClick(e: MouseEvent) {\n const target = e.target as Node\n if (\n menuRef.current &&\n !menuRef.current.contains(target) &&\n btnRef.current &&\n !btnRef.current.contains(target)\n ) {\n setOpen(false)\n }\n }\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n setOpen(false)\n btnRef.current?.focus()\n return\n }\n // Hotkey wiring: when the menu is open and focus is inside it, \u23CE/E/\u2318D trigger\n // the matching action. Skip when an editable surface owns focus.\n const active = document.activeElement as HTMLElement | null\n if (active) {\n const tag = active.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return\n }\n if (!menuRef.current?.contains(active)) return\n for (const item of items) {\n if (item.id && matchesShortcut(item.id, e)) {\n e.preventDefault()\n setOpen(false)\n item.onSelect?.()\n return\n }\n }\n }\n function onScrollOrResize() {\n updatePosition()\n }\n document.addEventListener('mousedown', onDocClick)\n document.addEventListener('keydown', onKey)\n window.addEventListener('scroll', onScrollOrResize, true)\n window.addEventListener('resize', onScrollOrResize)\n return () => {\n document.removeEventListener('mousedown', onDocClick)\n document.removeEventListener('keydown', onKey)\n window.removeEventListener('scroll', onScrollOrResize, true)\n window.removeEventListener('resize', onScrollOrResize)\n }\n }, [open, updatePosition])\n\n // Arrow-key navigation between menu items. Scoped to the menu so it doesn't fight with\n // global shortcuts when something else is focused.\n const handleMenuKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n menuRef.current?.querySelectorAll<HTMLElement>('[role=\"menuitem\"]') ?? [],\n )\n if (items.length === 0) return\n const currentIdx = items.findIndex((el) => el === document.activeElement)\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n const next = items[(currentIdx + 1 + items.length) % items.length]\n next?.focus()\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n const prev = items[(currentIdx - 1 + items.length) % items.length]\n prev?.focus()\n } else if (event.key === 'Home') {\n event.preventDefault()\n items[0]?.focus()\n } else if (event.key === 'End') {\n event.preventDefault()\n items[items.length - 1]?.focus()\n }\n }, [])\n\n if (!items.length) return null\n\n return (\n <div className=\"relative inline-flex\" data-card-action=\"true\" onClick={(event) => event.stopPropagation()}>\n <IconButton\n ref={btnRef}\n variant=\"ghost\"\n size=\"xs\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-label={ariaLabel}\n onClick={(event) => {\n event.stopPropagation()\n setOpen((previous) => !previous)\n requestAnimationFrame(updatePosition)\n }}\n className=\"inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MoreVertical className=\"size-3.5\" aria-hidden=\"true\" />\n </IconButton>\n {open && anchor && typeof document !== 'undefined'\n ? createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n onKeyDown={handleMenuKeyDown}\n className=\"fixed z-dropdown w-52 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-background p-1.5 shadow-md\"\n style={{\n top:\n direction === 'down'\n ? anchor.bottom + MENU_GAP\n : anchor.top - MENU_GAP,\n left: Math.min(anchor.right, window.innerWidth - 8),\n transform: `translate(-100%, ${direction === 'down' ? '0' : '-100%'})`,\n }}\n >\n {items.map((item, idx) => {\n const shortcut = item.id ? ITEM_SHORTCUTS[item.id] : null\n return (\n <Button\n variant={item.destructive ? 'destructive-ghost' : 'ghost'}\n size=\"sm\"\n key={item.id ?? `${item.label}-${idx}`}\n type=\"button\"\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n item.onSelect?.()\n }}\n className={`flex w-full items-center justify-start rounded-sm px-3 py-2 text-left text-sm leading-5 transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none ${\n item.destructive ? 'text-status-error-text hover:bg-status-error-bg/30' : 'text-foreground'\n }`}\n >\n <span className=\"flex-1 truncate\">{item.label}</span>\n {shortcut ? <MenuShortcut>{shortcut}</MenuShortcut> : null}\n </Button>\n )\n })}\n </div>,\n document.body,\n )\n : null}\n </div>\n )\n}\n\nexport default DealCardMenu\n"],
|
|
5
|
+
"mappings": ";AAuBI,cAkKc,YAlKd;AArBJ,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAQ3B,MAAM,WAAW;AAOjB,SAAS,aAAa,EAAE,SAAS,GAAsD;AACrF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,WAAU;AAAA,MAET;AAAA;AAAA,EACH;AAEJ;AAIA,MAAM,iBAAkD;AAAA,EACtD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,WAAW;AACb;AAEA,SAAS,gBAAgB,QAAgB,OAA+B;AACtE,MAAI,WAAW,UAAU,MAAM,QAAQ,QAAS,QAAO;AACvD,MAAI,WAAW,WAAW,MAAM,QAAQ,OAAO,MAAM,QAAQ,QAAQ,CAAC,MAAM,WAAW,CAAC,MAAM,QAAS,QAAO;AAC9G,MAAI,WAAW,gBAAgB,MAAM,QAAQ,OAAO,MAAM,QAAQ,SAAS,MAAM,WAAW,MAAM,SAAU,QAAO;AACnH,SAAO;AACT;AAEO,SAAS,aAAa,EAAE,OAAO,UAAU,GAAiD;AAC/F,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAyB,IAAI;AAC/D,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,MAAM;AACtE,QAAM,SAAS,MAAM,OAA0B,IAAI;AACnD,QAAM,UAAU,MAAM,OAAuB,IAAI;AAEjD,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,QAAI,CAAC,OAAO,QAAS;AACrB,UAAM,OAAO,OAAO,QAAQ,sBAAsB;AAClD,cAAU,IAAI;AACd,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,UAAM,aAAa,KAAK;AACxB,iBAAa,aAAa,OAAO,aAAa,aAAa,OAAO,MAAM;AAAA,EAC1E,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,mBAAe;AAGf,0BAAsB,MAAM;AAC1B,YAAM,QAAQ,QAAQ,SAAS,cAA2B,mBAAmB;AAC7E,aAAO,MAAM;AAAA,IACf,CAAC;AACD,aAAS,WAAW,GAAe;AACjC,YAAM,SAAS,EAAE;AACjB,UACE,QAAQ,WACR,CAAC,QAAQ,QAAQ,SAAS,MAAM,KAChC,OAAO,WACP,CAAC,OAAO,QAAQ,SAAS,MAAM,GAC/B;AACA,gBAAQ,KAAK;AAAA,MACf;AAAA,IACF;AACA,aAAS,MAAM,GAAkB;AAC/B,UAAI,EAAE,QAAQ,UAAU;AACtB,gBAAQ,KAAK;AACb,eAAO,SAAS,MAAM;AACtB;AAAA,MACF;AAGA,YAAM,SAAS,SAAS;AACxB,UAAI,QAAQ;AACV,cAAM,MAAM,OAAO;AACnB,YAAI,QAAQ,WAAW,QAAQ,cAAc,OAAO,kBAAmB;AAAA,MACzE;AACA,UAAI,CAAC,QAAQ,SAAS,SAAS,MAAM,EAAG;AACxC,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,MAAM,gBAAgB,KAAK,IAAI,CAAC,GAAG;AAC1C,YAAE,eAAe;AACjB,kBAAQ,KAAK;AACb,eAAK,WAAW;AAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,aAAS,mBAAmB;AAC1B,qBAAe;AAAA,IACjB;AACA,aAAS,iBAAiB,aAAa,UAAU;AACjD,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,iBAAiB,UAAU,kBAAkB,IAAI;AACxD,WAAO,iBAAiB,UAAU,gBAAgB;AAClD,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,UAAU;AACpD,eAAS,oBAAoB,WAAW,KAAK;AAC7C,aAAO,oBAAoB,UAAU,kBAAkB,IAAI;AAC3D,aAAO,oBAAoB,UAAU,gBAAgB;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,MAAM,cAAc,CAAC;AAIzB,QAAM,oBAAoB,MAAM,YAAY,CAAC,UAA+C;AAC1F,UAAMA,SAAQ,MAAM;AAAA,MAClB,QAAQ,SAAS,iBAA8B,mBAAmB,KAAK,CAAC;AAAA,IAC1E;AACA,QAAIA,OAAM,WAAW,EAAG;AACxB,UAAM,aAAaA,OAAM,UAAU,CAAC,OAAO,OAAO,SAAS,aAAa;AACxE,QAAI,MAAM,QAAQ,aAAa;AAC7B,YAAM,eAAe;AACrB,YAAM,OAAOA,QAAO,aAAa,IAAIA,OAAM,UAAUA,OAAM,MAAM;AACjE,YAAM,MAAM;AAAA,IACd,WAAW,MAAM,QAAQ,WAAW;AAClC,YAAM,eAAe;AACrB,YAAM,OAAOA,QAAO,aAAa,IAAIA,OAAM,UAAUA,OAAM,MAAM;AACjE,YAAM,MAAM;AAAA,IACd,WAAW,MAAM,QAAQ,QAAQ;AAC/B,YAAM,eAAe;AACrB,MAAAA,OAAM,CAAC,GAAG,MAAM;AAAA,IAClB,WAAW,MAAM,QAAQ,OAAO;AAC9B,YAAM,eAAe;AACrB,MAAAA,OAAMA,OAAM,SAAS,CAAC,GAAG,MAAM;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,MAAI,CAAC,MAAM,OAAQ,QAAO;AAE1B,SACE,qBAAC,SAAI,WAAU,wBAAuB,oBAAiB,QAAO,SAAS,CAAC,UAAU,MAAM,gBAAgB,GACtG;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,iBAAc;AAAA,QACd,iBAAe;AAAA,QACf,cAAY;AAAA,QACZ,SAAS,CAAC,UAAU;AAClB,gBAAM,gBAAgB;AACtB,kBAAQ,CAAC,aAAa,CAAC,QAAQ;AAC/B,gCAAsB,cAAc;AAAA,QACtC;AAAA,QACA,WAAU;AAAA,QAEV,8BAAC,gBAAa,WAAU,YAAW,eAAY,QAAO;AAAA;AAAA,IACxD;AAAA,IACC,QAAQ,UAAU,OAAO,aAAa,cACnC;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,WAAW;AAAA,UACX,WAAU;AAAA,UACV,OAAO;AAAA,YACL,KACE,cAAc,SACV,OAAO,SAAS,WAChB,OAAO,MAAM;AAAA,YACnB,MAAM,KAAK,IAAI,OAAO,OAAO,OAAO,aAAa,CAAC;AAAA,YAClD,WAAW,oBAAoB,cAAc,SAAS,MAAM,OAAO;AAAA,UACrE;AAAA,UAEC,gBAAM,IAAI,CAAC,MAAM,QAAQ;AACxB,kBAAM,WAAW,KAAK,KAAK,eAAe,KAAK,EAAE,IAAI;AACrD,mBACE;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS,KAAK,cAAc,sBAAsB;AAAA,gBAClD,MAAK;AAAA,gBAEL,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,SAAS,CAAC,UAAU;AAClB,wBAAM,gBAAgB;AACtB,0BAAQ,KAAK;AACb,uBAAK,WAAW;AAAA,gBAClB;AAAA,gBACA,WAAW,8KACT,KAAK,cAAc,uDAAuD,iBAC5E;AAAA,gBAEA;AAAA,sCAAC,UAAK,WAAU,mBAAmB,eAAK,OAAM;AAAA,kBAC7C,WAAW,oBAAC,gBAAc,oBAAS,IAAkB;AAAA;AAAA;AAAA,cAbjD,KAAK,MAAM,GAAG,KAAK,KAAK,IAAI,GAAG;AAAA,YActC;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH;AAAA,MACA,SAAS;AAAA,IACX,IACA;AAAA,KACN;AAEJ;AAEA,IAAO,uBAAQ;",
|
|
6
|
+
"names": ["items"]
|
|
7
|
+
}
|
package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, Search } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverTrigger
|
|
9
|
+
} from "@open-mercato/ui/primitives/popover";
|
|
10
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
11
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
12
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
13
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
14
|
+
import { ChipButton } from "./ChipButton.js";
|
|
15
|
+
import { FilterPopoverShell } from "./FilterPopoverShell.js";
|
|
16
|
+
const DEBOUNCE_MS = 250;
|
|
17
|
+
function EntityFilterPopover({
|
|
18
|
+
label,
|
|
19
|
+
values,
|
|
20
|
+
onApply,
|
|
21
|
+
initialOptions,
|
|
22
|
+
loadOptions,
|
|
23
|
+
labelById,
|
|
24
|
+
anyLabel,
|
|
25
|
+
title
|
|
26
|
+
}) {
|
|
27
|
+
const t = useT();
|
|
28
|
+
const [open, setOpen] = React.useState(false);
|
|
29
|
+
const [draft, setDraft] = React.useState(values);
|
|
30
|
+
const [query, setQuery] = React.useState("");
|
|
31
|
+
const [options, setOptions] = React.useState(initialOptions ?? []);
|
|
32
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
if (open) {
|
|
35
|
+
setDraft(values);
|
|
36
|
+
setQuery("");
|
|
37
|
+
setOptions(initialOptions ?? []);
|
|
38
|
+
}
|
|
39
|
+
}, [open, values, initialOptions]);
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
if (!open) return;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const handle = window.setTimeout(async () => {
|
|
44
|
+
setIsLoading(true);
|
|
45
|
+
try {
|
|
46
|
+
const items = await loadOptions(query, controller.signal);
|
|
47
|
+
setOptions(items);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (controller.signal.aborted) return;
|
|
50
|
+
} finally {
|
|
51
|
+
setIsLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, DEBOUNCE_MS);
|
|
54
|
+
return () => {
|
|
55
|
+
window.clearTimeout(handle);
|
|
56
|
+
controller.abort();
|
|
57
|
+
};
|
|
58
|
+
}, [open, query, loadOptions]);
|
|
59
|
+
const chipValue = React.useMemo(() => {
|
|
60
|
+
if (values.length === 0) {
|
|
61
|
+
return anyLabel ?? translateWithFallback(t, "customers.deals.kanban.filter.any", "Any");
|
|
62
|
+
}
|
|
63
|
+
if (values.length === 1) {
|
|
64
|
+
const id = values[0];
|
|
65
|
+
const fromLabels = labelById?.[id];
|
|
66
|
+
if (fromLabels) return fromLabels;
|
|
67
|
+
const fromOptions = options.find((option) => option.value === id)?.label;
|
|
68
|
+
if (fromOptions) return fromOptions;
|
|
69
|
+
return id.slice(0, 8);
|
|
70
|
+
}
|
|
71
|
+
return translateWithFallback(
|
|
72
|
+
t,
|
|
73
|
+
"customers.deals.kanban.filter.multipleSelected",
|
|
74
|
+
"{count} selected",
|
|
75
|
+
{ count: values.length }
|
|
76
|
+
);
|
|
77
|
+
}, [values, anyLabel, labelById, options, t]);
|
|
78
|
+
const toggleDraft = (id) => {
|
|
79
|
+
setDraft((prev) => prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]);
|
|
80
|
+
};
|
|
81
|
+
const handleApply = () => {
|
|
82
|
+
onApply(draft);
|
|
83
|
+
setOpen(false);
|
|
84
|
+
};
|
|
85
|
+
const handleClear = () => {
|
|
86
|
+
setDraft([]);
|
|
87
|
+
};
|
|
88
|
+
const handleKeyDown = (event) => {
|
|
89
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
handleApply();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const headerTitle = title ?? translateWithFallback(
|
|
95
|
+
t,
|
|
96
|
+
"customers.deals.kanban.filter.title",
|
|
97
|
+
"Filter \xB7 {label}",
|
|
98
|
+
{ label }
|
|
99
|
+
);
|
|
100
|
+
const footerLeft = /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
101
|
+
/* @__PURE__ */ jsx("span", { children: draft.length === 0 ? translateWithFallback(t, "customers.deals.kanban.filter.noneSelected", "None selected") : translateWithFallback(
|
|
102
|
+
t,
|
|
103
|
+
"customers.deals.kanban.filter.countSelected",
|
|
104
|
+
"{count} selected",
|
|
105
|
+
{ count: draft.length }
|
|
106
|
+
) }),
|
|
107
|
+
draft.length > 0 ? /* @__PURE__ */ jsx(
|
|
108
|
+
Button,
|
|
109
|
+
{
|
|
110
|
+
type: "button",
|
|
111
|
+
variant: "link",
|
|
112
|
+
size: "2xs",
|
|
113
|
+
onClick: handleClear,
|
|
114
|
+
className: "h-auto px-0 text-xs font-medium leading-normal text-muted-foreground hover:text-foreground",
|
|
115
|
+
children: translateWithFallback(t, "customers.deals.kanban.filter.clear", "Clear")
|
|
116
|
+
}
|
|
117
|
+
) : null
|
|
118
|
+
] });
|
|
119
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
120
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(ChipButton, { label, value: chipValue, active: values.length > 0 }) }),
|
|
121
|
+
/* @__PURE__ */ jsx(
|
|
122
|
+
PopoverContent,
|
|
123
|
+
{
|
|
124
|
+
className: "w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl",
|
|
125
|
+
align: "start",
|
|
126
|
+
onKeyDown: handleKeyDown,
|
|
127
|
+
children: /* @__PURE__ */ jsxs(
|
|
128
|
+
FilterPopoverShell,
|
|
129
|
+
{
|
|
130
|
+
title: headerTitle,
|
|
131
|
+
onClose: () => setOpen(false),
|
|
132
|
+
onCancel: () => setOpen(false),
|
|
133
|
+
onApply: handleApply,
|
|
134
|
+
footerLeft,
|
|
135
|
+
bodyClassName: "flex flex-col bg-card",
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border px-4 py-2.5", children: [
|
|
138
|
+
/* @__PURE__ */ jsx(Search, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }),
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
"input",
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
143
|
+
value: query,
|
|
144
|
+
onChange: (e) => setQuery(e.target.value),
|
|
145
|
+
placeholder: translateWithFallback(
|
|
146
|
+
t,
|
|
147
|
+
"customers.deals.kanban.filter.searchPlaceholder",
|
|
148
|
+
"Search\u2026"
|
|
149
|
+
),
|
|
150
|
+
className: "h-7 w-full bg-transparent text-sm leading-normal text-foreground outline-none placeholder:text-muted-foreground",
|
|
151
|
+
autoFocus: true
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
isLoading ? /* @__PURE__ */ jsx(Spinner, { className: "size-3" }) : null
|
|
155
|
+
] }),
|
|
156
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto", children: options.length === 0 && !isLoading ? /* @__PURE__ */ jsx("div", { className: "px-4 py-6 text-center text-xs text-muted-foreground", children: translateWithFallback(t, "customers.deals.kanban.filter.noResults", "No matches") }) : /* @__PURE__ */ jsx("ul", { className: "py-1", children: options.map((option) => {
|
|
157
|
+
const checked = draft.includes(option.value);
|
|
158
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
159
|
+
Button,
|
|
160
|
+
{
|
|
161
|
+
type: "button",
|
|
162
|
+
variant: "ghost",
|
|
163
|
+
role: "checkbox",
|
|
164
|
+
"aria-checked": checked,
|
|
165
|
+
onClick: () => toggleDraft(option.value),
|
|
166
|
+
className: `h-auto w-full justify-start gap-3 rounded-none px-4 py-2.5 text-left font-normal ${checked ? "bg-muted/60" : "bg-card"}`,
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ jsx(
|
|
169
|
+
"span",
|
|
170
|
+
{
|
|
171
|
+
className: `flex size-4 shrink-0 items-center justify-center rounded-sm border ${checked ? "border-accent-indigo bg-accent-indigo text-accent-indigo-foreground" : "border-input bg-card"}`,
|
|
172
|
+
"aria-hidden": "true",
|
|
173
|
+
children: checked ? /* @__PURE__ */ jsx(Check, { className: "size-3" }) : null
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsx(
|
|
177
|
+
"span",
|
|
178
|
+
{
|
|
179
|
+
className: `truncate text-sm leading-normal text-foreground ${checked ? "font-semibold" : "font-normal"}`,
|
|
180
|
+
children: option.label
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
) }, option.value);
|
|
186
|
+
}) }) })
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
] });
|
|
193
|
+
}
|
|
194
|
+
var EntityFilterPopover_default = EntityFilterPopover;
|
|
195
|
+
export {
|
|
196
|
+
EntityFilterPopover,
|
|
197
|
+
EntityFilterPopover_default as default
|
|
198
|
+
};
|
|
199
|
+
//# sourceMappingURL=EntityFilterPopover.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Check, Search } from 'lucide-react'\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { ChipButton } from './ChipButton'\nimport { FilterPopoverShell } from './FilterPopoverShell'\n\nexport type EntityFilterOption = {\n value: string\n label: string\n}\n\ntype EntityFilterPopoverProps = {\n /** Chip label (e.g. \"Owner\") */\n label: string\n /** Currently selected ids */\n values: string[]\n /** Apply selected ids back to parent */\n onApply: (next: string[]) => void\n /** Optional initial set of options to show before the user types */\n initialOptions?: EntityFilterOption[]\n /** Async loader called whenever the search query changes (debounced) */\n loadOptions: (query: string, signal: AbortSignal) => Promise<EntityFilterOption[]>\n /** Labels for selected ids \u2014 used to display the chip value when only ids are known */\n labelById?: Record<string, string>\n /** Placeholder shown when nothing is selected */\n anyLabel?: string\n /** Title for the popover header */\n title?: string\n}\n\nconst DEBOUNCE_MS = 250\n\n/**\n * Reusable async-search multi-select popover used by Owner / People / Companies kanban filter\n * chips. Wraps the shared FilterPopoverShell so the chrome (rounded-2xl container, white header\n * with bold title, muted footer with Cancel + Apply) matches the SPEC-048 mock surfaces for the\n * Status (1045:11861) and Pipeline (1045:11917) popovers.\n */\nexport function EntityFilterPopover({\n label,\n values,\n onApply,\n initialOptions,\n loadOptions,\n labelById,\n anyLabel,\n title,\n}: EntityFilterPopoverProps): React.ReactElement {\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const [draft, setDraft] = React.useState<string[]>(values)\n const [query, setQuery] = React.useState('')\n const [options, setOptions] = React.useState<EntityFilterOption[]>(initialOptions ?? [])\n const [isLoading, setIsLoading] = React.useState(false)\n\n // Reset draft to current applied state whenever the popover opens\n React.useEffect(() => {\n if (open) {\n setDraft(values)\n setQuery('')\n setOptions(initialOptions ?? [])\n }\n }, [open, values, initialOptions])\n\n // Debounced async option loader\n React.useEffect(() => {\n if (!open) return\n const controller = new AbortController()\n const handle = window.setTimeout(async () => {\n setIsLoading(true)\n try {\n const items = await loadOptions(query, controller.signal)\n setOptions(items)\n } catch (err) {\n if (controller.signal.aborted) return\n } finally {\n setIsLoading(false)\n }\n }, DEBOUNCE_MS)\n return () => {\n window.clearTimeout(handle)\n controller.abort()\n }\n }, [open, query, loadOptions])\n\n const chipValue = React.useMemo(() => {\n if (values.length === 0) {\n return anyLabel ?? translateWithFallback(t, 'customers.deals.kanban.filter.any', 'Any')\n }\n if (values.length === 1) {\n const id = values[0]\n const fromLabels = labelById?.[id]\n if (fromLabels) return fromLabels\n const fromOptions = options.find((option) => option.value === id)?.label\n if (fromOptions) return fromOptions\n return id.slice(0, 8)\n }\n return translateWithFallback(\n t,\n 'customers.deals.kanban.filter.multipleSelected',\n '{count} selected',\n { count: values.length },\n )\n }, [values, anyLabel, labelById, options, t])\n\n const toggleDraft = (id: string) => {\n setDraft((prev) => (prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]))\n }\n\n const handleApply = () => {\n onApply(draft)\n setOpen(false)\n }\n const handleClear = () => {\n setDraft([])\n }\n\n // Cmd/Ctrl+Enter from anywhere inside the popover confirms \u2014 parity with the dialog\n // primary-action shortcut (`AGENTS.md` UI Interaction rules). Also works while typing\n // in the search box because keydown bubbles up to the PopoverContent root.\n const handleKeyDown = (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleApply()\n }\n }\n\n const headerTitle = title ?? translateWithFallback(\n t,\n 'customers.deals.kanban.filter.title',\n 'Filter \u00B7 {label}',\n { label },\n )\n\n const footerLeft = (\n <div className=\"flex items-center gap-3\">\n <span>\n {draft.length === 0\n ? translateWithFallback(t, 'customers.deals.kanban.filter.noneSelected', 'None selected')\n : translateWithFallback(\n t,\n 'customers.deals.kanban.filter.countSelected',\n '{count} selected',\n { count: draft.length },\n )}\n </span>\n {draft.length > 0 ? (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n onClick={handleClear}\n className=\"h-auto px-0 text-xs font-medium leading-normal text-muted-foreground hover:text-foreground\"\n >\n {translateWithFallback(t, 'customers.deals.kanban.filter.clear', 'Clear')}\n </Button>\n ) : null}\n </div>\n )\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <ChipButton label={label} value={chipValue} active={values.length > 0} />\n </PopoverTrigger>\n <PopoverContent\n className=\"w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl\"\n align=\"start\"\n onKeyDown={handleKeyDown}\n >\n <FilterPopoverShell\n title={headerTitle}\n onClose={() => setOpen(false)}\n onCancel={() => setOpen(false)}\n onApply={handleApply}\n footerLeft={footerLeft}\n bodyClassName=\"flex flex-col bg-card\"\n >\n <div className=\"flex items-center gap-2 border-b border-border px-4 py-2.5\">\n <Search className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translateWithFallback(\n t,\n 'customers.deals.kanban.filter.searchPlaceholder',\n 'Search\u2026',\n )}\n className=\"h-7 w-full bg-transparent text-sm leading-normal text-foreground outline-none placeholder:text-muted-foreground\"\n autoFocus\n />\n {isLoading ? <Spinner className=\"size-3\" /> : null}\n </div>\n\n <div className=\"max-h-64 overflow-y-auto\">\n {options.length === 0 && !isLoading ? (\n <div className=\"px-4 py-6 text-center text-xs text-muted-foreground\">\n {translateWithFallback(t, 'customers.deals.kanban.filter.noResults', 'No matches')}\n </div>\n ) : (\n <ul className=\"py-1\">\n {options.map((option) => {\n const checked = draft.includes(option.value)\n return (\n <li key={option.value}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n role=\"checkbox\"\n aria-checked={checked}\n onClick={() => toggleDraft(option.value)}\n className={`h-auto w-full justify-start gap-3 rounded-none px-4 py-2.5 text-left font-normal ${\n checked ? 'bg-muted/60' : 'bg-card'\n }`}\n >\n <span\n className={`flex size-4 shrink-0 items-center justify-center rounded-sm border ${\n checked\n ? 'border-accent-indigo bg-accent-indigo text-accent-indigo-foreground'\n : 'border-input bg-card'\n }`}\n aria-hidden=\"true\"\n >\n {checked ? <Check className=\"size-3\" /> : null}\n </span>\n <span\n className={`truncate text-sm leading-normal text-foreground ${\n checked ? 'font-semibold' : 'font-normal'\n }`}\n >\n {option.label}\n </span>\n </Button>\n </li>\n )\n })}\n </ul>\n )}\n </div>\n </FilterPopoverShell>\n </PopoverContent>\n </Popover>\n )\n}\n\nexport default EntityFilterPopover\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
5
|
+
import { ChipButton } from "./ChipButton.js";
|
|
6
|
+
function FilterBarRow({
|
|
7
|
+
leadingChips,
|
|
8
|
+
chips,
|
|
9
|
+
sortNode,
|
|
10
|
+
onChipClick
|
|
11
|
+
}) {
|
|
12
|
+
const t = useT();
|
|
13
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-y-2 rounded-lg bg-muted/40 px-3.5 py-3", children: [
|
|
14
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
15
|
+
/* @__PURE__ */ jsxs("span", { className: "text-overline font-bold uppercase leading-normal text-muted-foreground", children: [
|
|
16
|
+
translateWithFallback(t, "customers.deals.kanban.filter.label", "Filter"),
|
|
17
|
+
":"
|
|
18
|
+
] }),
|
|
19
|
+
leadingChips,
|
|
20
|
+
chips.map((chip) => /* @__PURE__ */ jsx(
|
|
21
|
+
ChipButton,
|
|
22
|
+
{
|
|
23
|
+
label: chip.label,
|
|
24
|
+
value: chip.value,
|
|
25
|
+
onClick: () => onChipClick(chip.id),
|
|
26
|
+
ariaLabel: translateWithFallback(
|
|
27
|
+
t,
|
|
28
|
+
"customers.deals.kanban.filter.aria.chip",
|
|
29
|
+
"Filter by {label}",
|
|
30
|
+
{ label: chip.label }
|
|
31
|
+
)
|
|
32
|
+
},
|
|
33
|
+
chip.id
|
|
34
|
+
))
|
|
35
|
+
] }),
|
|
36
|
+
/* @__PURE__ */ jsx("div", { className: "ml-auto", children: sortNode })
|
|
37
|
+
] });
|
|
38
|
+
}
|
|
39
|
+
var FilterBarRow_default = FilterBarRow;
|
|
40
|
+
export {
|
|
41
|
+
FilterBarRow,
|
|
42
|
+
FilterBarRow_default as default
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=FilterBarRow.js.map
|
package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { ChipButton } from './ChipButton'\n\nexport type KanbanFilterChip = {\n id: 'owner' | 'people' | 'companies' | 'close'\n label: string\n value: string\n}\n\ntype FilterBarRowProps = {\n /** Popover-backed chips (Status, Pipeline) rendered first, in order */\n leadingChips: React.ReactNode\n /** Static (Phase-5-stubbed) chips */\n chips: KanbanFilterChip[]\n /** Right-aligned sort chip (popover-backed) */\n sortNode: React.ReactNode\n onChipClick: (chipId: KanbanFilterChip['id']) => void\n}\n\nexport function FilterBarRow({\n leadingChips,\n chips,\n sortNode,\n onChipClick,\n}: FilterBarRowProps): React.ReactElement {\n const t = useT()\n // Note: the Figma mock includes a \"+ Add filter\" chip (1045:11987) that opens a picker for\n // Tags / Source / Currency / Created date / Updated date / Last activity / custom fields.\n // That picker is not wired up in this slice (it would need AdvancedFilterPanel integration),\n // so the affordance is intentionally omitted instead of shipping a \"coming next\" toast.\n return (\n <div className=\"flex flex-wrap items-center justify-between gap-y-2 rounded-lg bg-muted/40 px-3.5 py-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-overline font-bold uppercase leading-normal text-muted-foreground\">\n {translateWithFallback(t, 'customers.deals.kanban.filter.label', 'Filter')}:\n </span>\n {leadingChips}\n {chips.map((chip) => (\n <ChipButton\n key={chip.id}\n label={chip.label}\n value={chip.value}\n onClick={() => onChipClick(chip.id)}\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.filter.aria.chip',\n 'Filter by {label}',\n { label: chip.label },\n )}\n />\n ))}\n </div>\n <div className=\"ml-auto\">{sortNode}</div>\n </div>\n )\n}\n\nexport default FilterBarRow\n"],
|
|
5
|
+
"mappings": ";AAqCQ,SAKE,KALF;AAlCR,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAkBpB,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0C;AACxC,QAAM,IAAI,KAAK;AAKf,SACE,qBAAC,SAAI,WAAU,0FACb;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA,2BAAC,UAAK,WAAU,0EACb;AAAA,8BAAsB,GAAG,uCAAuC,QAAQ;AAAA,QAAE;AAAA,SAC7E;AAAA,MACC;AAAA,MACA,MAAM,IAAI,CAAC,SACV;AAAA,QAAC;AAAA;AAAA,UAEC,OAAO,KAAK;AAAA,UACZ,OAAO,KAAK;AAAA,UACZ,SAAS,MAAM,YAAY,KAAK,EAAE;AAAA,UAClC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,OAAO,KAAK,MAAM;AAAA,UACtB;AAAA;AAAA,QATK,KAAK;AAAA,MAUZ,CACD;AAAA,OACH;AAAA,IACA,oBAAC,SAAI,WAAU,WAAW,oBAAS;AAAA,KACrC;AAEJ;AAEA,IAAO,uBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|