@open-mercato/core 0.6.3-develop.3635.1.e4a05c16e8 → 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,214 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
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
|
+
import type { RowActionItem } from '@open-mercato/ui/backend/RowActions'
|
|
9
|
+
|
|
10
|
+
type DealCardMenuProps = {
|
|
11
|
+
items: RowActionItem[]
|
|
12
|
+
ariaLabel: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MENU_GAP = 6
|
|
16
|
+
|
|
17
|
+
// Right-aligned shortcut hint inside a menu row. Mirrors the Figma deal-card menu
|
|
18
|
+
// (node 1045:12254), which shows ⏎ for Open, E for Edit, ⌘D for Duplicate.
|
|
19
|
+
// `aria-hidden="true"` keeps the kbd out of the menu item's accessible name —
|
|
20
|
+
// without this the menu item reads as "Open deal ↵" instead of "Open deal" and
|
|
21
|
+
// `getByRole('menuitem', { name: 'Open deal', exact: true })` fails in tests.
|
|
22
|
+
function MenuShortcut({ children }: { children: React.ReactNode }): React.ReactElement {
|
|
23
|
+
return (
|
|
24
|
+
<kbd
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
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"
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</kbd>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Map of menu-item id → keyboard shortcut hint. Stay in sync with `buildMenuItems` in
|
|
34
|
+
// `pipeline/page.tsx` and with `matchesShortcut` below.
|
|
35
|
+
const ITEM_SHORTCUTS: Record<string, React.ReactNode> = {
|
|
36
|
+
open: '↵',
|
|
37
|
+
edit: 'E',
|
|
38
|
+
duplicate: '⌘D',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function matchesShortcut(itemId: string, event: KeyboardEvent): boolean {
|
|
42
|
+
if (itemId === 'open' && event.key === 'Enter') return true
|
|
43
|
+
if (itemId === 'edit' && (event.key === 'e' || event.key === 'E') && !event.metaKey && !event.ctrlKey) return true
|
|
44
|
+
if (itemId === 'duplicate' && (event.key === 'd' || event.key === 'D') && (event.metaKey || event.ctrlKey)) return true
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function DealCardMenu({ items, ariaLabel }: DealCardMenuProps): React.ReactElement | null {
|
|
49
|
+
const [open, setOpen] = React.useState(false)
|
|
50
|
+
const [anchor, setAnchor] = React.useState<DOMRect | null>(null)
|
|
51
|
+
const [direction, setDirection] = React.useState<'down' | 'up'>('down')
|
|
52
|
+
const btnRef = React.useRef<HTMLButtonElement>(null)
|
|
53
|
+
const menuRef = React.useRef<HTMLDivElement>(null)
|
|
54
|
+
|
|
55
|
+
const updatePosition = React.useCallback(() => {
|
|
56
|
+
if (!btnRef.current) return
|
|
57
|
+
const rect = btnRef.current.getBoundingClientRect()
|
|
58
|
+
setAnchor(rect)
|
|
59
|
+
const spaceBelow = window.innerHeight - rect.bottom
|
|
60
|
+
const spaceAbove = rect.top
|
|
61
|
+
setDirection(spaceBelow < 220 && spaceAbove > spaceBelow ? 'up' : 'down')
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (!open) return
|
|
66
|
+
updatePosition()
|
|
67
|
+
// Autofocus the first menu item once the portal has painted. We schedule this on the
|
|
68
|
+
// next frame so the `createPortal` render has actually mounted the buttons in the DOM.
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
const first = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]')
|
|
71
|
+
first?.focus()
|
|
72
|
+
})
|
|
73
|
+
function onDocClick(e: MouseEvent) {
|
|
74
|
+
const target = e.target as Node
|
|
75
|
+
if (
|
|
76
|
+
menuRef.current &&
|
|
77
|
+
!menuRef.current.contains(target) &&
|
|
78
|
+
btnRef.current &&
|
|
79
|
+
!btnRef.current.contains(target)
|
|
80
|
+
) {
|
|
81
|
+
setOpen(false)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function onKey(e: KeyboardEvent) {
|
|
85
|
+
if (e.key === 'Escape') {
|
|
86
|
+
setOpen(false)
|
|
87
|
+
btnRef.current?.focus()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
// Hotkey wiring: when the menu is open and focus is inside it, ⏎/E/⌘D trigger
|
|
91
|
+
// the matching action. Skip when an editable surface owns focus.
|
|
92
|
+
const active = document.activeElement as HTMLElement | null
|
|
93
|
+
if (active) {
|
|
94
|
+
const tag = active.tagName
|
|
95
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return
|
|
96
|
+
}
|
|
97
|
+
if (!menuRef.current?.contains(active)) return
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
if (item.id && matchesShortcut(item.id, e)) {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
setOpen(false)
|
|
102
|
+
item.onSelect?.()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function onScrollOrResize() {
|
|
108
|
+
updatePosition()
|
|
109
|
+
}
|
|
110
|
+
document.addEventListener('mousedown', onDocClick)
|
|
111
|
+
document.addEventListener('keydown', onKey)
|
|
112
|
+
window.addEventListener('scroll', onScrollOrResize, true)
|
|
113
|
+
window.addEventListener('resize', onScrollOrResize)
|
|
114
|
+
return () => {
|
|
115
|
+
document.removeEventListener('mousedown', onDocClick)
|
|
116
|
+
document.removeEventListener('keydown', onKey)
|
|
117
|
+
window.removeEventListener('scroll', onScrollOrResize, true)
|
|
118
|
+
window.removeEventListener('resize', onScrollOrResize)
|
|
119
|
+
}
|
|
120
|
+
}, [open, updatePosition])
|
|
121
|
+
|
|
122
|
+
// Arrow-key navigation between menu items. Scoped to the menu so it doesn't fight with
|
|
123
|
+
// global shortcuts when something else is focused.
|
|
124
|
+
const handleMenuKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
125
|
+
const items = Array.from(
|
|
126
|
+
menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [],
|
|
127
|
+
)
|
|
128
|
+
if (items.length === 0) return
|
|
129
|
+
const currentIdx = items.findIndex((el) => el === document.activeElement)
|
|
130
|
+
if (event.key === 'ArrowDown') {
|
|
131
|
+
event.preventDefault()
|
|
132
|
+
const next = items[(currentIdx + 1 + items.length) % items.length]
|
|
133
|
+
next?.focus()
|
|
134
|
+
} else if (event.key === 'ArrowUp') {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
const prev = items[(currentIdx - 1 + items.length) % items.length]
|
|
137
|
+
prev?.focus()
|
|
138
|
+
} else if (event.key === 'Home') {
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
items[0]?.focus()
|
|
141
|
+
} else if (event.key === 'End') {
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
items[items.length - 1]?.focus()
|
|
144
|
+
}
|
|
145
|
+
}, [])
|
|
146
|
+
|
|
147
|
+
if (!items.length) return null
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="relative inline-flex" data-card-action="true" onClick={(event) => event.stopPropagation()}>
|
|
151
|
+
<IconButton
|
|
152
|
+
ref={btnRef}
|
|
153
|
+
variant="ghost"
|
|
154
|
+
size="xs"
|
|
155
|
+
aria-haspopup="menu"
|
|
156
|
+
aria-expanded={open}
|
|
157
|
+
aria-label={ariaLabel}
|
|
158
|
+
onClick={(event) => {
|
|
159
|
+
event.stopPropagation()
|
|
160
|
+
setOpen((previous) => !previous)
|
|
161
|
+
requestAnimationFrame(updatePosition)
|
|
162
|
+
}}
|
|
163
|
+
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"
|
|
164
|
+
>
|
|
165
|
+
<MoreVertical className="size-3.5" aria-hidden="true" />
|
|
166
|
+
</IconButton>
|
|
167
|
+
{open && anchor && typeof document !== 'undefined'
|
|
168
|
+
? createPortal(
|
|
169
|
+
<div
|
|
170
|
+
ref={menuRef}
|
|
171
|
+
role="menu"
|
|
172
|
+
onKeyDown={handleMenuKeyDown}
|
|
173
|
+
className="fixed z-dropdown w-52 max-w-[calc(100vw-1rem)] rounded-md border border-border bg-background p-1.5 shadow-md"
|
|
174
|
+
style={{
|
|
175
|
+
top:
|
|
176
|
+
direction === 'down'
|
|
177
|
+
? anchor.bottom + MENU_GAP
|
|
178
|
+
: anchor.top - MENU_GAP,
|
|
179
|
+
left: Math.min(anchor.right, window.innerWidth - 8),
|
|
180
|
+
transform: `translate(-100%, ${direction === 'down' ? '0' : '-100%'})`,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{items.map((item, idx) => {
|
|
184
|
+
const shortcut = item.id ? ITEM_SHORTCUTS[item.id] : null
|
|
185
|
+
return (
|
|
186
|
+
<Button
|
|
187
|
+
variant={item.destructive ? 'destructive-ghost' : 'ghost'}
|
|
188
|
+
size="sm"
|
|
189
|
+
key={item.id ?? `${item.label}-${idx}`}
|
|
190
|
+
type="button"
|
|
191
|
+
role="menuitem"
|
|
192
|
+
onClick={(event) => {
|
|
193
|
+
event.stopPropagation()
|
|
194
|
+
setOpen(false)
|
|
195
|
+
item.onSelect?.()
|
|
196
|
+
}}
|
|
197
|
+
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 ${
|
|
198
|
+
item.destructive ? 'text-status-error-text hover:bg-status-error-bg/30' : 'text-foreground'
|
|
199
|
+
}`}
|
|
200
|
+
>
|
|
201
|
+
<span className="flex-1 truncate">{item.label}</span>
|
|
202
|
+
{shortcut ? <MenuShortcut>{shortcut}</MenuShortcut> : null}
|
|
203
|
+
</Button>
|
|
204
|
+
)
|
|
205
|
+
})}
|
|
206
|
+
</div>,
|
|
207
|
+
document.body,
|
|
208
|
+
)
|
|
209
|
+
: null}
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default DealCardMenu
|
package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
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'
|
|
15
|
+
import { FilterPopoverShell } from './FilterPopoverShell'
|
|
16
|
+
|
|
17
|
+
export type EntityFilterOption = {
|
|
18
|
+
value: string
|
|
19
|
+
label: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type EntityFilterPopoverProps = {
|
|
23
|
+
/** Chip label (e.g. "Owner") */
|
|
24
|
+
label: string
|
|
25
|
+
/** Currently selected ids */
|
|
26
|
+
values: string[]
|
|
27
|
+
/** Apply selected ids back to parent */
|
|
28
|
+
onApply: (next: string[]) => void
|
|
29
|
+
/** Optional initial set of options to show before the user types */
|
|
30
|
+
initialOptions?: EntityFilterOption[]
|
|
31
|
+
/** Async loader called whenever the search query changes (debounced) */
|
|
32
|
+
loadOptions: (query: string, signal: AbortSignal) => Promise<EntityFilterOption[]>
|
|
33
|
+
/** Labels for selected ids — used to display the chip value when only ids are known */
|
|
34
|
+
labelById?: Record<string, string>
|
|
35
|
+
/** Placeholder shown when nothing is selected */
|
|
36
|
+
anyLabel?: string
|
|
37
|
+
/** Title for the popover header */
|
|
38
|
+
title?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEBOUNCE_MS = 250
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reusable async-search multi-select popover used by Owner / People / Companies kanban filter
|
|
45
|
+
* chips. Wraps the shared FilterPopoverShell so the chrome (rounded-2xl container, white header
|
|
46
|
+
* with bold title, muted footer with Cancel + Apply) matches the SPEC-048 mock surfaces for the
|
|
47
|
+
* Status (1045:11861) and Pipeline (1045:11917) popovers.
|
|
48
|
+
*/
|
|
49
|
+
export function EntityFilterPopover({
|
|
50
|
+
label,
|
|
51
|
+
values,
|
|
52
|
+
onApply,
|
|
53
|
+
initialOptions,
|
|
54
|
+
loadOptions,
|
|
55
|
+
labelById,
|
|
56
|
+
anyLabel,
|
|
57
|
+
title,
|
|
58
|
+
}: EntityFilterPopoverProps): React.ReactElement {
|
|
59
|
+
const t = useT()
|
|
60
|
+
const [open, setOpen] = React.useState(false)
|
|
61
|
+
const [draft, setDraft] = React.useState<string[]>(values)
|
|
62
|
+
const [query, setQuery] = React.useState('')
|
|
63
|
+
const [options, setOptions] = React.useState<EntityFilterOption[]>(initialOptions ?? [])
|
|
64
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
65
|
+
|
|
66
|
+
// Reset draft to current applied state whenever the popover opens
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (open) {
|
|
69
|
+
setDraft(values)
|
|
70
|
+
setQuery('')
|
|
71
|
+
setOptions(initialOptions ?? [])
|
|
72
|
+
}
|
|
73
|
+
}, [open, values, initialOptions])
|
|
74
|
+
|
|
75
|
+
// Debounced async option loader
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (!open) return
|
|
78
|
+
const controller = new AbortController()
|
|
79
|
+
const handle = window.setTimeout(async () => {
|
|
80
|
+
setIsLoading(true)
|
|
81
|
+
try {
|
|
82
|
+
const items = await loadOptions(query, controller.signal)
|
|
83
|
+
setOptions(items)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (controller.signal.aborted) return
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false)
|
|
88
|
+
}
|
|
89
|
+
}, DEBOUNCE_MS)
|
|
90
|
+
return () => {
|
|
91
|
+
window.clearTimeout(handle)
|
|
92
|
+
controller.abort()
|
|
93
|
+
}
|
|
94
|
+
}, [open, query, loadOptions])
|
|
95
|
+
|
|
96
|
+
const chipValue = React.useMemo(() => {
|
|
97
|
+
if (values.length === 0) {
|
|
98
|
+
return anyLabel ?? translateWithFallback(t, 'customers.deals.kanban.filter.any', 'Any')
|
|
99
|
+
}
|
|
100
|
+
if (values.length === 1) {
|
|
101
|
+
const id = values[0]
|
|
102
|
+
const fromLabels = labelById?.[id]
|
|
103
|
+
if (fromLabels) return fromLabels
|
|
104
|
+
const fromOptions = options.find((option) => option.value === id)?.label
|
|
105
|
+
if (fromOptions) return fromOptions
|
|
106
|
+
return id.slice(0, 8)
|
|
107
|
+
}
|
|
108
|
+
return translateWithFallback(
|
|
109
|
+
t,
|
|
110
|
+
'customers.deals.kanban.filter.multipleSelected',
|
|
111
|
+
'{count} selected',
|
|
112
|
+
{ count: values.length },
|
|
113
|
+
)
|
|
114
|
+
}, [values, anyLabel, labelById, options, t])
|
|
115
|
+
|
|
116
|
+
const toggleDraft = (id: string) => {
|
|
117
|
+
setDraft((prev) => (prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const handleApply = () => {
|
|
121
|
+
onApply(draft)
|
|
122
|
+
setOpen(false)
|
|
123
|
+
}
|
|
124
|
+
const handleClear = () => {
|
|
125
|
+
setDraft([])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Cmd/Ctrl+Enter from anywhere inside the popover confirms — parity with the dialog
|
|
129
|
+
// primary-action shortcut (`AGENTS.md` UI Interaction rules). Also works while typing
|
|
130
|
+
// in the search box because keydown bubbles up to the PopoverContent root.
|
|
131
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
132
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
handleApply()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const headerTitle = title ?? translateWithFallback(
|
|
139
|
+
t,
|
|
140
|
+
'customers.deals.kanban.filter.title',
|
|
141
|
+
'Filter · {label}',
|
|
142
|
+
{ label },
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const footerLeft = (
|
|
146
|
+
<div className="flex items-center gap-3">
|
|
147
|
+
<span>
|
|
148
|
+
{draft.length === 0
|
|
149
|
+
? translateWithFallback(t, 'customers.deals.kanban.filter.noneSelected', 'None selected')
|
|
150
|
+
: translateWithFallback(
|
|
151
|
+
t,
|
|
152
|
+
'customers.deals.kanban.filter.countSelected',
|
|
153
|
+
'{count} selected',
|
|
154
|
+
{ count: draft.length },
|
|
155
|
+
)}
|
|
156
|
+
</span>
|
|
157
|
+
{draft.length > 0 ? (
|
|
158
|
+
<Button
|
|
159
|
+
type="button"
|
|
160
|
+
variant="link"
|
|
161
|
+
size="2xs"
|
|
162
|
+
onClick={handleClear}
|
|
163
|
+
className="h-auto px-0 text-xs font-medium leading-normal text-muted-foreground hover:text-foreground"
|
|
164
|
+
>
|
|
165
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.clear', 'Clear')}
|
|
166
|
+
</Button>
|
|
167
|
+
) : null}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
173
|
+
<PopoverTrigger asChild>
|
|
174
|
+
<ChipButton label={label} value={chipValue} active={values.length > 0} />
|
|
175
|
+
</PopoverTrigger>
|
|
176
|
+
<PopoverContent
|
|
177
|
+
className="w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl"
|
|
178
|
+
align="start"
|
|
179
|
+
onKeyDown={handleKeyDown}
|
|
180
|
+
>
|
|
181
|
+
<FilterPopoverShell
|
|
182
|
+
title={headerTitle}
|
|
183
|
+
onClose={() => setOpen(false)}
|
|
184
|
+
onCancel={() => setOpen(false)}
|
|
185
|
+
onApply={handleApply}
|
|
186
|
+
footerLeft={footerLeft}
|
|
187
|
+
bodyClassName="flex flex-col bg-card"
|
|
188
|
+
>
|
|
189
|
+
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
|
190
|
+
<Search className="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
191
|
+
<input
|
|
192
|
+
type="text"
|
|
193
|
+
value={query}
|
|
194
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
195
|
+
placeholder={translateWithFallback(
|
|
196
|
+
t,
|
|
197
|
+
'customers.deals.kanban.filter.searchPlaceholder',
|
|
198
|
+
'Search…',
|
|
199
|
+
)}
|
|
200
|
+
className="h-7 w-full bg-transparent text-sm leading-normal text-foreground outline-none placeholder:text-muted-foreground"
|
|
201
|
+
autoFocus
|
|
202
|
+
/>
|
|
203
|
+
{isLoading ? <Spinner className="size-3" /> : null}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div className="max-h-64 overflow-y-auto">
|
|
207
|
+
{options.length === 0 && !isLoading ? (
|
|
208
|
+
<div className="px-4 py-6 text-center text-xs text-muted-foreground">
|
|
209
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.noResults', 'No matches')}
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
<ul className="py-1">
|
|
213
|
+
{options.map((option) => {
|
|
214
|
+
const checked = draft.includes(option.value)
|
|
215
|
+
return (
|
|
216
|
+
<li key={option.value}>
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
variant="ghost"
|
|
220
|
+
role="checkbox"
|
|
221
|
+
aria-checked={checked}
|
|
222
|
+
onClick={() => toggleDraft(option.value)}
|
|
223
|
+
className={`h-auto w-full justify-start gap-3 rounded-none px-4 py-2.5 text-left font-normal ${
|
|
224
|
+
checked ? 'bg-muted/60' : 'bg-card'
|
|
225
|
+
}`}
|
|
226
|
+
>
|
|
227
|
+
<span
|
|
228
|
+
className={`flex size-4 shrink-0 items-center justify-center rounded-sm border ${
|
|
229
|
+
checked
|
|
230
|
+
? 'border-accent-indigo bg-accent-indigo text-accent-indigo-foreground'
|
|
231
|
+
: 'border-input bg-card'
|
|
232
|
+
}`}
|
|
233
|
+
aria-hidden="true"
|
|
234
|
+
>
|
|
235
|
+
{checked ? <Check className="size-3" /> : null}
|
|
236
|
+
</span>
|
|
237
|
+
<span
|
|
238
|
+
className={`truncate text-sm leading-normal text-foreground ${
|
|
239
|
+
checked ? 'font-semibold' : 'font-normal'
|
|
240
|
+
}`}
|
|
241
|
+
>
|
|
242
|
+
{option.label}
|
|
243
|
+
</span>
|
|
244
|
+
</Button>
|
|
245
|
+
</li>
|
|
246
|
+
)
|
|
247
|
+
})}
|
|
248
|
+
</ul>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</FilterPopoverShell>
|
|
252
|
+
</PopoverContent>
|
|
253
|
+
</Popover>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default EntityFilterPopover
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
6
|
+
import { ChipButton } from './ChipButton'
|
|
7
|
+
|
|
8
|
+
export type KanbanFilterChip = {
|
|
9
|
+
id: 'owner' | 'people' | 'companies' | 'close'
|
|
10
|
+
label: string
|
|
11
|
+
value: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type FilterBarRowProps = {
|
|
15
|
+
/** Popover-backed chips (Status, Pipeline) rendered first, in order */
|
|
16
|
+
leadingChips: React.ReactNode
|
|
17
|
+
/** Static (Phase-5-stubbed) chips */
|
|
18
|
+
chips: KanbanFilterChip[]
|
|
19
|
+
/** Right-aligned sort chip (popover-backed) */
|
|
20
|
+
sortNode: React.ReactNode
|
|
21
|
+
onChipClick: (chipId: KanbanFilterChip['id']) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function FilterBarRow({
|
|
25
|
+
leadingChips,
|
|
26
|
+
chips,
|
|
27
|
+
sortNode,
|
|
28
|
+
onChipClick,
|
|
29
|
+
}: FilterBarRowProps): React.ReactElement {
|
|
30
|
+
const t = useT()
|
|
31
|
+
// Note: the Figma mock includes a "+ Add filter" chip (1045:11987) that opens a picker for
|
|
32
|
+
// Tags / Source / Currency / Created date / Updated date / Last activity / custom fields.
|
|
33
|
+
// That picker is not wired up in this slice (it would need AdvancedFilterPanel integration),
|
|
34
|
+
// so the affordance is intentionally omitted instead of shipping a "coming next" toast.
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-wrap items-center justify-between gap-y-2 rounded-lg bg-muted/40 px-3.5 py-3">
|
|
37
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
38
|
+
<span className="text-overline font-bold uppercase leading-normal text-muted-foreground">
|
|
39
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.label', 'Filter')}:
|
|
40
|
+
</span>
|
|
41
|
+
{leadingChips}
|
|
42
|
+
{chips.map((chip) => (
|
|
43
|
+
<ChipButton
|
|
44
|
+
key={chip.id}
|
|
45
|
+
label={chip.label}
|
|
46
|
+
value={chip.value}
|
|
47
|
+
onClick={() => onChipClick(chip.id)}
|
|
48
|
+
ariaLabel={translateWithFallback(
|
|
49
|
+
t,
|
|
50
|
+
'customers.deals.kanban.filter.aria.chip',
|
|
51
|
+
'Filter by {label}',
|
|
52
|
+
{ label: chip.label },
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="ml-auto">{sortNode}</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default FilterBarRow
|
package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { X } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
7
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
9
|
+
|
|
10
|
+
type FilterPopoverShellProps = {
|
|
11
|
+
/** Title rendered in the header (e.g. "Filter · Status", "Sort by") */
|
|
12
|
+
title: React.ReactNode
|
|
13
|
+
/** Optional icon rendered before the title (Sort by uses a tag icon per Figma) */
|
|
14
|
+
leadingIcon?: React.ReactNode
|
|
15
|
+
/** Body content — sits between header and footer */
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
/** Footer left content — usually a "N selected" hint or "Default: …" subtitle */
|
|
18
|
+
footerLeft?: React.ReactNode
|
|
19
|
+
/** Called when the X close button is clicked */
|
|
20
|
+
onClose: () => void
|
|
21
|
+
/** Called when Cancel is clicked */
|
|
22
|
+
onCancel: () => void
|
|
23
|
+
/** Called when Apply (primary CTA) is clicked */
|
|
24
|
+
onApply: () => void
|
|
25
|
+
/** Override the primary CTA label (e.g. "Add" on the +Add-filter popover) */
|
|
26
|
+
applyLabel?: string
|
|
27
|
+
/** Body wrapper className override — defaults to standard padded white block */
|
|
28
|
+
bodyClassName?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared chrome for every kanban filter popover (Status, Pipeline, Sort, Owner, People,
|
|
33
|
+
* Companies, Close). Matches the SPEC-048 Figma popover surfaces (nodes 1045:11861,
|
|
34
|
+
* 1045:11917, 1045:12090): rounded-2xl outer, white header w/ bold title + close button,
|
|
35
|
+
* white body, muted footer with Cancel + Apply.
|
|
36
|
+
*
|
|
37
|
+
* Each consumer renders its own body — checkboxes, pills, radio rows, date picker, etc. —
|
|
38
|
+
* inside this shell, so the chrome stays identical across every filter surface and the
|
|
39
|
+
* styling delta lives only inside `children`.
|
|
40
|
+
*/
|
|
41
|
+
export function FilterPopoverShell({
|
|
42
|
+
title,
|
|
43
|
+
leadingIcon,
|
|
44
|
+
children,
|
|
45
|
+
footerLeft,
|
|
46
|
+
onClose,
|
|
47
|
+
onCancel,
|
|
48
|
+
onApply,
|
|
49
|
+
applyLabel,
|
|
50
|
+
bodyClassName,
|
|
51
|
+
}: FilterPopoverShellProps): React.ReactElement {
|
|
52
|
+
const t = useT()
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col overflow-hidden rounded-2xl bg-card">
|
|
55
|
+
<div className="flex items-center justify-between border-b border-border bg-card px-5 py-4">
|
|
56
|
+
<div className="flex items-center gap-2">
|
|
57
|
+
{leadingIcon ? (
|
|
58
|
+
<span className="flex size-4 items-center justify-center text-muted-foreground" aria-hidden="true">
|
|
59
|
+
{leadingIcon}
|
|
60
|
+
</span>
|
|
61
|
+
) : null}
|
|
62
|
+
<span className="text-base font-bold leading-normal text-foreground">{title}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<IconButton
|
|
65
|
+
type="button"
|
|
66
|
+
variant="outline"
|
|
67
|
+
size="sm"
|
|
68
|
+
onClick={onClose}
|
|
69
|
+
aria-label={translateWithFallback(t, 'customers.deals.kanban.filter.close', 'Close')}
|
|
70
|
+
>
|
|
71
|
+
<X className="size-3.5" aria-hidden="true" />
|
|
72
|
+
</IconButton>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className={bodyClassName ?? 'flex flex-col gap-4 bg-card px-5 py-4'}>{children}</div>
|
|
76
|
+
|
|
77
|
+
<div className="flex items-center justify-between border-t border-border bg-muted px-5 py-3.5">
|
|
78
|
+
<div className="text-xs leading-normal text-muted-foreground">{footerLeft ?? null}</div>
|
|
79
|
+
<div className="flex items-center gap-6">
|
|
80
|
+
<Button
|
|
81
|
+
variant="outline"
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={onCancel}
|
|
84
|
+
className="h-auto rounded-lg border-input bg-card px-4 py-2 text-sm font-semibold text-foreground"
|
|
85
|
+
>
|
|
86
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.cancel', 'Cancel')}
|
|
87
|
+
</Button>
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={onApply}
|
|
91
|
+
className="h-auto rounded-lg bg-foreground px-5 py-2 text-sm font-semibold text-background hover:bg-foreground/90"
|
|
92
|
+
>
|
|
93
|
+
{applyLabel ?? translateWithFallback(t, 'customers.deals.kanban.filter.apply', 'Apply')}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default FilterPopoverShell
|