@open-mercato/core 0.6.3-develop.3651.1.d3a548a5f1 → 0.6.3-develop.3660.1.53b73d5aa4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_settings/index.js +2 -0
- package/dist/generated/entities/customer_settings/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
- package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
- package/dist/modules/customers/api/deals/route.js +255 -122
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
- package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +2 -0
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
- package/dist/modules/customers/commands/deals.js +6 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +48 -13
- package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
- package/dist/modules/customers/commands/settings.js +45 -5
- package/dist/modules/customers/commands/settings.js.map +2 -2
- package/dist/modules/customers/data/enrichers.js +130 -1
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +4 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +29 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/bulkDeals.js +150 -0
- package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js +12 -0
- package/dist/modules/customers/lib/kysely.js.map +7 -0
- package/dist/modules/customers/lib/stuckDeals.js +32 -0
- package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
- package/generated/entities/customer_settings/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
- package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
- package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
- package/src/modules/customers/api/deals/route.ts +330 -135
- package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
- package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
- package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
- package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
- package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
- package/src/modules/customers/commands/deals.ts +19 -0
- package/src/modules/customers/commands/pipeline-stages.ts +68 -13
- package/src/modules/customers/commands/settings.ts +64 -5
- package/src/modules/customers/data/enrichers.ts +209 -2
- package/src/modules/customers/data/entities.ts +4 -1
- package/src/modules/customers/data/validators.ts +36 -2
- package/src/modules/customers/i18n/de.json +244 -0
- package/src/modules/customers/i18n/en.json +244 -0
- package/src/modules/customers/i18n/es.json +244 -0
- package/src/modules/customers/i18n/pl.json +244 -0
- package/src/modules/customers/lib/bulkDeals.ts +231 -0
- package/src/modules/customers/lib/kysely.ts +60 -0
- package/src/modules/customers/lib/stuckDeals.ts +88 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
- package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
- package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
- package/src/modules/customers/workers/deals-bulk-update-stage.ts +46 -0
package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverContent,
|
|
7
|
+
PopoverTrigger,
|
|
8
|
+
} from '@open-mercato/ui/primitives/popover'
|
|
9
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
11
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
12
|
+
import { ChipButton } from './ChipButton'
|
|
13
|
+
import { FilterPopoverShell } from './FilterPopoverShell'
|
|
14
|
+
|
|
15
|
+
export type CloseDateRange = {
|
|
16
|
+
from: string | null
|
|
17
|
+
to: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CloseDateFilterPopoverProps = {
|
|
21
|
+
value: CloseDateRange
|
|
22
|
+
onApply: (next: CloseDateRange) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type PresetKey = 'next-30' | 'next-90' | 'this-month' | 'this-quarter' | 'overdue' | 'custom'
|
|
26
|
+
|
|
27
|
+
function ymd(date: Date): string {
|
|
28
|
+
return date.toISOString().slice(0, 10)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function startOfMonth(): Date {
|
|
32
|
+
const d = new Date()
|
|
33
|
+
d.setUTCHours(0, 0, 0, 0)
|
|
34
|
+
d.setUTCDate(1)
|
|
35
|
+
return d
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function endOfMonth(): Date {
|
|
39
|
+
const d = startOfMonth()
|
|
40
|
+
d.setUTCMonth(d.getUTCMonth() + 1)
|
|
41
|
+
d.setUTCDate(0)
|
|
42
|
+
return d
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function startOfQuarter(): Date {
|
|
46
|
+
const d = new Date()
|
|
47
|
+
d.setUTCHours(0, 0, 0, 0)
|
|
48
|
+
d.setUTCDate(1)
|
|
49
|
+
const m = d.getUTCMonth()
|
|
50
|
+
d.setUTCMonth(m - (m % 3))
|
|
51
|
+
return d
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function endOfQuarter(): Date {
|
|
55
|
+
const d = startOfQuarter()
|
|
56
|
+
d.setUTCMonth(d.getUTCMonth() + 3)
|
|
57
|
+
d.setUTCDate(0)
|
|
58
|
+
return d
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildPresetRange(key: PresetKey): CloseDateRange {
|
|
62
|
+
const now = new Date()
|
|
63
|
+
now.setUTCHours(0, 0, 0, 0)
|
|
64
|
+
switch (key) {
|
|
65
|
+
case 'next-30': {
|
|
66
|
+
const to = new Date(now)
|
|
67
|
+
to.setUTCDate(to.getUTCDate() + 30)
|
|
68
|
+
return { from: ymd(now), to: ymd(to) }
|
|
69
|
+
}
|
|
70
|
+
case 'next-90': {
|
|
71
|
+
const to = new Date(now)
|
|
72
|
+
to.setUTCDate(to.getUTCDate() + 90)
|
|
73
|
+
return { from: ymd(now), to: ymd(to) }
|
|
74
|
+
}
|
|
75
|
+
case 'this-month':
|
|
76
|
+
return { from: ymd(startOfMonth()), to: ymd(endOfMonth()) }
|
|
77
|
+
case 'this-quarter':
|
|
78
|
+
return { from: ymd(startOfQuarter()), to: ymd(endOfQuarter()) }
|
|
79
|
+
case 'overdue': {
|
|
80
|
+
const yesterday = new Date(now)
|
|
81
|
+
yesterday.setUTCDate(yesterday.getUTCDate() - 1)
|
|
82
|
+
return { from: null, to: ymd(yesterday) }
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
return { from: null, to: null }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function CloseDateFilterPopover({ value, onApply }: CloseDateFilterPopoverProps): React.ReactElement {
|
|
90
|
+
const t = useT()
|
|
91
|
+
const [open, setOpen] = React.useState(false)
|
|
92
|
+
const [draft, setDraft] = React.useState<CloseDateRange>(value)
|
|
93
|
+
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
if (open) setDraft(value)
|
|
96
|
+
}, [open, value])
|
|
97
|
+
|
|
98
|
+
const isActive = !!(value.from || value.to)
|
|
99
|
+
const chipLabel = translateWithFallback(t, 'customers.deals.kanban.filter.close', 'Close')
|
|
100
|
+
const chipValue = React.useMemo(() => {
|
|
101
|
+
// Show "Any" (not "Next 90 days") when no filter is applied — the previous label was a
|
|
102
|
+
// preset name, which read like the filter was already active and confused operators.
|
|
103
|
+
if (!isActive) return translateWithFallback(t, 'customers.deals.kanban.filter.any', 'Any')
|
|
104
|
+
if (value.from && value.to) {
|
|
105
|
+
const f = new Date(value.from).toLocaleDateString(undefined, { month: 'short', day: '2-digit' })
|
|
106
|
+
const tt = new Date(value.to).toLocaleDateString(undefined, { month: 'short', day: '2-digit' })
|
|
107
|
+
return `${f} – ${tt}`
|
|
108
|
+
}
|
|
109
|
+
if (value.from && !value.to)
|
|
110
|
+
return translateWithFallback(t, 'customers.deals.kanban.filter.from', 'From {date}', {
|
|
111
|
+
date: new Date(value.from).toLocaleDateString(undefined, { month: 'short', day: '2-digit' }),
|
|
112
|
+
})
|
|
113
|
+
if (!value.from && value.to)
|
|
114
|
+
return translateWithFallback(t, 'customers.deals.kanban.filter.until', 'Until {date}', {
|
|
115
|
+
date: new Date(value.to).toLocaleDateString(undefined, { month: 'short', day: '2-digit' }),
|
|
116
|
+
})
|
|
117
|
+
return ''
|
|
118
|
+
}, [value.from, value.to, isActive, t])
|
|
119
|
+
|
|
120
|
+
const applyPreset = (key: PresetKey) => setDraft(buildPresetRange(key))
|
|
121
|
+
|
|
122
|
+
const handleApply = () => {
|
|
123
|
+
onApply({
|
|
124
|
+
from: draft.from && draft.from.length ? draft.from : null,
|
|
125
|
+
to: draft.to && draft.to.length ? draft.to : null,
|
|
126
|
+
})
|
|
127
|
+
setOpen(false)
|
|
128
|
+
}
|
|
129
|
+
const handleClear = () => setDraft({ from: null, to: null })
|
|
130
|
+
|
|
131
|
+
// Cmd/Ctrl+Enter from anywhere inside the popover confirms — parity with the dialog
|
|
132
|
+
// primary-action shortcut (`AGENTS.md` UI Interaction rules).
|
|
133
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
134
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
handleApply()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hasDraft = !!(draft.from || draft.to)
|
|
141
|
+
const footerLeft = (
|
|
142
|
+
<div className="flex items-center gap-3">
|
|
143
|
+
<span>
|
|
144
|
+
{hasDraft
|
|
145
|
+
? translateWithFallback(t, 'customers.deals.kanban.filter.dateRange.applied', 'Range set')
|
|
146
|
+
: translateWithFallback(t, 'customers.deals.kanban.filter.any', 'Any')}
|
|
147
|
+
</span>
|
|
148
|
+
{hasDraft ? (
|
|
149
|
+
<Button
|
|
150
|
+
variant="link"
|
|
151
|
+
size="sm"
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={handleClear}
|
|
154
|
+
className="h-auto px-0 py-0 text-xs font-medium leading-normal"
|
|
155
|
+
>
|
|
156
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.clear', 'Clear')}
|
|
157
|
+
</Button>
|
|
158
|
+
) : null}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
164
|
+
<PopoverTrigger asChild>
|
|
165
|
+
<ChipButton label={chipLabel} value={chipValue} active={isActive} />
|
|
166
|
+
</PopoverTrigger>
|
|
167
|
+
<PopoverContent
|
|
168
|
+
className="w-80 rounded-2xl border-border bg-transparent p-0 shadow-xl"
|
|
169
|
+
align="start"
|
|
170
|
+
onKeyDown={handleKeyDown}
|
|
171
|
+
>
|
|
172
|
+
<FilterPopoverShell
|
|
173
|
+
title={translateWithFallback(t, 'customers.deals.kanban.filter.close.title', 'Filter · Expected close')}
|
|
174
|
+
onClose={() => setOpen(false)}
|
|
175
|
+
onCancel={() => setOpen(false)}
|
|
176
|
+
onApply={handleApply}
|
|
177
|
+
footerLeft={footerLeft}
|
|
178
|
+
>
|
|
179
|
+
<div className="flex flex-wrap gap-1.5">
|
|
180
|
+
{(
|
|
181
|
+
[
|
|
182
|
+
{ key: 'next-30', label: translateWithFallback(t, 'customers.deals.kanban.filter.preset.next30', 'Next 30 days') },
|
|
183
|
+
{ key: 'next-90', label: translateWithFallback(t, 'customers.deals.kanban.filter.preset.next90', 'Next 90 days') },
|
|
184
|
+
{ key: 'this-month', label: translateWithFallback(t, 'customers.deals.kanban.filter.preset.thisMonth', 'This month') },
|
|
185
|
+
{ key: 'this-quarter', label: translateWithFallback(t, 'customers.deals.kanban.filter.preset.thisQuarter', 'This quarter') },
|
|
186
|
+
{ key: 'overdue', label: translateWithFallback(t, 'customers.deals.kanban.filter.preset.overdue', 'Overdue') },
|
|
187
|
+
] as Array<{ key: PresetKey; label: string }>
|
|
188
|
+
).map((preset) => (
|
|
189
|
+
<Button
|
|
190
|
+
variant="outline"
|
|
191
|
+
size="sm"
|
|
192
|
+
key={preset.key}
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => applyPreset(preset.key)}
|
|
195
|
+
className="h-auto rounded-full border-border bg-card px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
196
|
+
>
|
|
197
|
+
{preset.label}
|
|
198
|
+
</Button>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div className="grid grid-cols-2 gap-3">
|
|
203
|
+
<label className="flex flex-col gap-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
204
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.fromLabel', 'From')}
|
|
205
|
+
<input
|
|
206
|
+
type="date"
|
|
207
|
+
value={draft.from ?? ''}
|
|
208
|
+
onChange={(event) =>
|
|
209
|
+
setDraft((previous) => ({ ...previous, from: event.target.value || null }))
|
|
210
|
+
}
|
|
211
|
+
className="rounded-md border border-input bg-card px-2.5 py-1.5 text-sm font-normal leading-normal text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
212
|
+
/>
|
|
213
|
+
</label>
|
|
214
|
+
<label className="flex flex-col gap-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
215
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.toLabel', 'To')}
|
|
216
|
+
<input
|
|
217
|
+
type="date"
|
|
218
|
+
value={draft.to ?? ''}
|
|
219
|
+
onChange={(event) =>
|
|
220
|
+
setDraft((previous) => ({ ...previous, to: event.target.value || null }))
|
|
221
|
+
}
|
|
222
|
+
className="rounded-md border border-input bg-card px-2.5 py-1.5 text-sm font-normal leading-normal text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
223
|
+
/>
|
|
224
|
+
</label>
|
|
225
|
+
</div>
|
|
226
|
+
</FilterPopoverShell>
|
|
227
|
+
</PopoverContent>
|
|
228
|
+
</Popover>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default CloseDateFilterPopover
|
package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { AlertTriangle } from 'lucide-react'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
7
|
+
|
|
8
|
+
export type CurrencyBreakdownRow = {
|
|
9
|
+
currency: string
|
|
10
|
+
total: number
|
|
11
|
+
count: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type CurrencyBreakdownTableProps = {
|
|
15
|
+
rows: CurrencyBreakdownRow[]
|
|
16
|
+
baseCurrencyCode: string | null
|
|
17
|
+
/** Currencies with no FX rate to base — surfaced with a warning icon next to the code. */
|
|
18
|
+
missingRateCurrencies: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read-only currency breakdown body shared between `LaneCurrencyBreakdown` and any future
|
|
23
|
+
* informational popover. Matches the SPEC-048 Figma node 1251:610 currency table:
|
|
24
|
+
* - Column header row: "Currency · Native total · Deals" (overline gray)
|
|
25
|
+
* - Divider line
|
|
26
|
+
* - Per-currency rows with thin bottom dividers; BASE pill on the base-currency row
|
|
27
|
+
*
|
|
28
|
+
* The interactive filter variant (`CurrencyFilterPopover`) does NOT use this table — its rows
|
|
29
|
+
* are radio selectors with different highlighting — but it reuses the same outer chrome from
|
|
30
|
+
* `LaneCurrencyBreakdown`. Keeping them visually consistent is the responsibility of the
|
|
31
|
+
* Figma source of truth, not of this component.
|
|
32
|
+
*/
|
|
33
|
+
export function CurrencyBreakdownTable({
|
|
34
|
+
rows,
|
|
35
|
+
baseCurrencyCode,
|
|
36
|
+
missingRateCurrencies,
|
|
37
|
+
}: CurrencyBreakdownTableProps): React.ReactElement {
|
|
38
|
+
const t = useT()
|
|
39
|
+
const base = baseCurrencyCode?.toUpperCase() ?? null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col bg-card px-6">
|
|
43
|
+
<div className="flex items-center gap-1 pt-5 pb-2">
|
|
44
|
+
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
45
|
+
{translateWithFallback(t, 'customers.deals.kanban.currencyBreakdown.colCurrency', 'Currency')}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="flex-1" aria-hidden="true" />
|
|
48
|
+
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
49
|
+
{translateWithFallback(
|
|
50
|
+
t,
|
|
51
|
+
'customers.deals.kanban.currencyBreakdown.colNativeTotal',
|
|
52
|
+
'Native total',
|
|
53
|
+
)}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="pl-6 text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
56
|
+
{translateWithFallback(t, 'customers.deals.kanban.currencyBreakdown.colDeals', 'Deals')}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="border-b border-border" aria-hidden="true" />
|
|
60
|
+
|
|
61
|
+
<ul className="flex flex-col">
|
|
62
|
+
{rows.map((row) => {
|
|
63
|
+
const code = row.currency.toUpperCase()
|
|
64
|
+
const isBase = base !== null && code === base
|
|
65
|
+
const isMissingRate = missingRateCurrencies.includes(row.currency)
|
|
66
|
+
return (
|
|
67
|
+
<li
|
|
68
|
+
key={row.currency}
|
|
69
|
+
className="flex items-center gap-1 border-b border-border py-2.5 last:border-b-0"
|
|
70
|
+
>
|
|
71
|
+
<span className="flex items-center gap-1.5 text-sm font-semibold leading-normal text-foreground">
|
|
72
|
+
{isMissingRate ? (
|
|
73
|
+
<AlertTriangle
|
|
74
|
+
className="size-3.5 text-status-warning-icon"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
/>
|
|
77
|
+
) : null}
|
|
78
|
+
<span>{code}</span>
|
|
79
|
+
{isBase ? (
|
|
80
|
+
// text-[9px] is an intentional Figma exception for the BASE badge (node 1251:671).
|
|
81
|
+
// See CurrencyFilterPopover for the same rationale — both share the badge.
|
|
82
|
+
<span className="inline-flex items-center rounded-sm bg-brand-violet/10 px-1.5 py-px text-[9px] font-bold uppercase leading-normal tracking-wider text-brand-violet">
|
|
83
|
+
{translateWithFallback(t, 'customers.deals.kanban.currencyBreakdown.basePill', 'BASE')}
|
|
84
|
+
</span>
|
|
85
|
+
) : null}
|
|
86
|
+
</span>
|
|
87
|
+
<span className="flex-1" aria-hidden="true" />
|
|
88
|
+
<span className="text-sm font-semibold leading-normal text-foreground tabular-nums">
|
|
89
|
+
{formatAmount(row.total)}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="pl-6 text-sm font-normal leading-normal text-muted-foreground tabular-nums">
|
|
92
|
+
{row.count}
|
|
93
|
+
</span>
|
|
94
|
+
</li>
|
|
95
|
+
)
|
|
96
|
+
})}
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatAmount(amount: number): string {
|
|
103
|
+
return new Intl.NumberFormat(undefined, {
|
|
104
|
+
style: 'decimal',
|
|
105
|
+
maximumFractionDigits: 0,
|
|
106
|
+
useGrouping: true,
|
|
107
|
+
}).format(Math.round(amount))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default CurrencyBreakdownTable
|
package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { X } 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 { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
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
|
+
|
|
16
|
+
export type CurrencyFilterRow = {
|
|
17
|
+
currency: string
|
|
18
|
+
total: number
|
|
19
|
+
count: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type CurrencyFilterPopoverProps = {
|
|
23
|
+
/** Per-currency rows scoped to the current pipeline filter context. */
|
|
24
|
+
rows: CurrencyFilterRow[]
|
|
25
|
+
/** Tenant base currency code, or `null` when none is configured. */
|
|
26
|
+
baseCurrencyCode: string | null
|
|
27
|
+
/** Aggregate total across all currencies in `rows`, expressed in `baseCurrencyCode`. */
|
|
28
|
+
totalInBaseCurrency: number
|
|
29
|
+
/** Stage / pipeline label shown in the subtitle (e.g. `"PIPELINE"`). */
|
|
30
|
+
headingLabel: string
|
|
31
|
+
/** Total deal count surfaced in the subtitle. */
|
|
32
|
+
headingCount: number
|
|
33
|
+
/** Currently-applied currency filter (`null` means "All currencies"). */
|
|
34
|
+
selectedCurrency: string | null
|
|
35
|
+
/** Called when the operator presses "Apply filter". `null` clears the filter. */
|
|
36
|
+
onApply: (currency: string | null) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Currency filter popover anchored to a "Currency" chip on the kanban filter bar.
|
|
41
|
+
*
|
|
42
|
+
* Matches SPEC-048 Figma node 1251:699 — header + subtitle + radio rows + Apply filter footer.
|
|
43
|
+
* Selecting a row narrows the kanban board to deals whose `valueCurrency` equals the chosen
|
|
44
|
+
* code; the "All currencies" sentinel clears the filter. The selected row uses the
|
|
45
|
+
* brand-violet tinted background to mirror the Figma highlight.
|
|
46
|
+
*
|
|
47
|
+
* Chrome (rounded-2xl outer, bordered header w/ close, muted footer) matches
|
|
48
|
+
* `LaneCurrencyBreakdown` so the two popovers feel like a family.
|
|
49
|
+
*/
|
|
50
|
+
export function CurrencyFilterPopover({
|
|
51
|
+
rows,
|
|
52
|
+
baseCurrencyCode,
|
|
53
|
+
totalInBaseCurrency,
|
|
54
|
+
headingLabel,
|
|
55
|
+
headingCount,
|
|
56
|
+
selectedCurrency,
|
|
57
|
+
onApply,
|
|
58
|
+
}: CurrencyFilterPopoverProps): React.ReactElement {
|
|
59
|
+
const t = useT()
|
|
60
|
+
const [open, setOpen] = React.useState(false)
|
|
61
|
+
const [draft, setDraft] = React.useState<string | null>(selectedCurrency)
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (open) setDraft(selectedCurrency)
|
|
65
|
+
}, [open, selectedCurrency])
|
|
66
|
+
|
|
67
|
+
const base = baseCurrencyCode?.toUpperCase() ?? null
|
|
68
|
+
const chipLabel = translateWithFallback(t, 'customers.deals.kanban.currencyFilter.chipLabel', 'Currency')
|
|
69
|
+
const chipValue = selectedCurrency
|
|
70
|
+
? selectedCurrency.toUpperCase()
|
|
71
|
+
: translateWithFallback(t, 'customers.deals.kanban.filter.all', 'All')
|
|
72
|
+
const closeLabel = translateWithFallback(t, 'customers.deals.kanban.filter.close', 'Close')
|
|
73
|
+
const applyLabel = translateWithFallback(
|
|
74
|
+
t,
|
|
75
|
+
'customers.deals.kanban.currencyFilter.apply',
|
|
76
|
+
'Apply filter',
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const handleApply = () => {
|
|
80
|
+
onApply(draft)
|
|
81
|
+
setOpen(false)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cmd/Ctrl+Enter parity with other kanban filter popovers (`AGENTS.md` UI Interaction rules).
|
|
85
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
86
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
87
|
+
event.preventDefault()
|
|
88
|
+
handleApply()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const headingCountLabel = translateWithFallback(
|
|
93
|
+
t,
|
|
94
|
+
'customers.deals.kanban.currencyBreakdown.headingCount',
|
|
95
|
+
'({count} deals)',
|
|
96
|
+
{ count: headingCount },
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
101
|
+
<PopoverTrigger asChild>
|
|
102
|
+
<ChipButton
|
|
103
|
+
label={chipLabel}
|
|
104
|
+
value={chipValue}
|
|
105
|
+
active={selectedCurrency !== null}
|
|
106
|
+
/>
|
|
107
|
+
</PopoverTrigger>
|
|
108
|
+
<PopoverContent
|
|
109
|
+
align="start"
|
|
110
|
+
className="w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl"
|
|
111
|
+
onKeyDown={handleKeyDown}
|
|
112
|
+
>
|
|
113
|
+
{/*
|
|
114
|
+
Solid `bg-card` (no opacity modifier) on the outer wrapper — round-2 UX review item 33.
|
|
115
|
+
Floating overlays must not let underlying card content bleed through; the previous
|
|
116
|
+
`bg-muted/30` allowed card titles/badges to show through this popover.
|
|
117
|
+
*/}
|
|
118
|
+
<div className="flex flex-col overflow-hidden rounded-2xl bg-card">
|
|
119
|
+
<div className="flex items-center justify-between border-b border-border bg-card px-5 py-4">
|
|
120
|
+
<span className="text-base font-bold leading-normal text-foreground">
|
|
121
|
+
{translateWithFallback(
|
|
122
|
+
t,
|
|
123
|
+
'customers.deals.kanban.currencyFilter.title',
|
|
124
|
+
'Filter kanban by currency',
|
|
125
|
+
)}
|
|
126
|
+
</span>
|
|
127
|
+
<IconButton
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="xs"
|
|
130
|
+
onClick={() => setOpen(false)}
|
|
131
|
+
aria-label={closeLabel}
|
|
132
|
+
className="flex size-7 items-center justify-center rounded-md border border-border bg-card text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
133
|
+
>
|
|
134
|
+
<X className="size-3.5" aria-hidden="true" />
|
|
135
|
+
</IconButton>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className="flex flex-col bg-card pt-3.5">
|
|
139
|
+
<div className="px-6 pb-2">
|
|
140
|
+
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
141
|
+
{translateWithFallback(
|
|
142
|
+
t,
|
|
143
|
+
'customers.deals.kanban.currencyFilter.subtitle',
|
|
144
|
+
'{label} · {count} deals · {total} {currency}',
|
|
145
|
+
{
|
|
146
|
+
label: headingLabel,
|
|
147
|
+
count: headingCount,
|
|
148
|
+
total: formatAmount(totalInBaseCurrency),
|
|
149
|
+
currency: baseCurrencyCode ?? '',
|
|
150
|
+
},
|
|
151
|
+
)}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="flex flex-col">
|
|
156
|
+
<CurrencyRow
|
|
157
|
+
isAllCurrencies
|
|
158
|
+
selected={draft === null}
|
|
159
|
+
onClick={() => setDraft(null)}
|
|
160
|
+
label={translateWithFallback(
|
|
161
|
+
t,
|
|
162
|
+
'customers.deals.kanban.currencyFilter.allCurrencies',
|
|
163
|
+
'All currencies',
|
|
164
|
+
)}
|
|
165
|
+
amount={totalInBaseCurrency}
|
|
166
|
+
amountCurrency={baseCurrencyCode ?? ''}
|
|
167
|
+
count={headingCount}
|
|
168
|
+
/>
|
|
169
|
+
{rows.map((row) => {
|
|
170
|
+
const code = row.currency.toUpperCase()
|
|
171
|
+
const isBase = base !== null && code === base
|
|
172
|
+
const isSelected = draft === code
|
|
173
|
+
return (
|
|
174
|
+
<CurrencyRow
|
|
175
|
+
key={row.currency}
|
|
176
|
+
selected={isSelected}
|
|
177
|
+
onClick={() => setDraft(code)}
|
|
178
|
+
label={code}
|
|
179
|
+
showBasePill={isBase}
|
|
180
|
+
amount={row.total}
|
|
181
|
+
amountCurrency={code}
|
|
182
|
+
count={row.count}
|
|
183
|
+
/>
|
|
184
|
+
)
|
|
185
|
+
})}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="px-6 pt-3 pb-3.5">
|
|
189
|
+
<p className="text-xs font-normal leading-normal text-muted-foreground">
|
|
190
|
+
{translateWithFallback(
|
|
191
|
+
t,
|
|
192
|
+
'customers.deals.kanban.currencyFilter.footerCredit',
|
|
193
|
+
'NBP mid conversion rate · {date}',
|
|
194
|
+
{ date: formatToday() },
|
|
195
|
+
)}
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Footer uses solid `bg-muted` (no opacity modifier) — round-2 UX review item 33. */}
|
|
201
|
+
<div className="flex items-center justify-end border-t border-border bg-muted px-5 py-3.5">
|
|
202
|
+
<Button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={handleApply}
|
|
205
|
+
className="h-auto rounded-lg bg-foreground px-5 py-2 text-sm font-semibold text-background hover:bg-foreground/90"
|
|
206
|
+
>
|
|
207
|
+
{applyLabel}
|
|
208
|
+
</Button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</PopoverContent>
|
|
212
|
+
</Popover>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type CurrencyRowProps = {
|
|
217
|
+
/** Marks the row as the "All currencies" sentinel so we suppress the per-row currency suffix. */
|
|
218
|
+
isAllCurrencies?: boolean
|
|
219
|
+
selected: boolean
|
|
220
|
+
onClick: () => void
|
|
221
|
+
label: string
|
|
222
|
+
showBasePill?: boolean
|
|
223
|
+
amount: number
|
|
224
|
+
amountCurrency: string
|
|
225
|
+
count: number
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function CurrencyRow({
|
|
229
|
+
isAllCurrencies = false,
|
|
230
|
+
selected,
|
|
231
|
+
onClick,
|
|
232
|
+
label,
|
|
233
|
+
showBasePill = false,
|
|
234
|
+
amount,
|
|
235
|
+
amountCurrency,
|
|
236
|
+
count,
|
|
237
|
+
}: CurrencyRowProps): React.ReactElement {
|
|
238
|
+
const t = useT()
|
|
239
|
+
const rowBg = selected ? 'bg-brand-violet/10' : 'bg-card hover:bg-muted'
|
|
240
|
+
const codeColor = selected ? 'text-brand-violet' : 'text-foreground'
|
|
241
|
+
const amountColor = selected ? 'text-brand-violet font-semibold' : 'text-foreground font-normal'
|
|
242
|
+
const currencyColor = selected ? 'text-brand-violet/80' : 'text-muted-foreground'
|
|
243
|
+
const countColor = selected ? 'text-brand-violet font-semibold' : 'text-foreground/80 font-normal'
|
|
244
|
+
return (
|
|
245
|
+
<Button
|
|
246
|
+
type="button"
|
|
247
|
+
variant="ghost"
|
|
248
|
+
role="radio"
|
|
249
|
+
aria-checked={selected}
|
|
250
|
+
onClick={onClick}
|
|
251
|
+
className={`h-auto w-full justify-start gap-3 rounded-none px-6 py-2.5 text-left font-normal ${rowBg}`}
|
|
252
|
+
>
|
|
253
|
+
<RadioDot selected={selected} />
|
|
254
|
+
<span className="flex items-center gap-1.5">
|
|
255
|
+
<span className={`text-sm leading-normal ${codeColor} ${selected ? 'font-semibold' : 'font-normal'}`}>
|
|
256
|
+
{label}
|
|
257
|
+
</span>
|
|
258
|
+
{showBasePill ? (
|
|
259
|
+
// text-[9px] is an intentional Figma exception for the BASE badge (node 1251:671).
|
|
260
|
+
// The DS scale bottoms out at text-xs (12px); this 9px is design-system tracked as
|
|
261
|
+
// a tiny-badge exception and should not be migrated to text-overline.
|
|
262
|
+
<span className="inline-flex items-center rounded-sm bg-muted px-1.5 py-px text-[9px] font-bold uppercase leading-normal tracking-wider text-muted-foreground">
|
|
263
|
+
{translateWithFallback(t, 'customers.deals.kanban.currencyBreakdown.basePill', 'BASE')}
|
|
264
|
+
</span>
|
|
265
|
+
) : null}
|
|
266
|
+
</span>
|
|
267
|
+
<span className="flex-1" aria-hidden="true" />
|
|
268
|
+
<span className="flex items-baseline gap-1">
|
|
269
|
+
<span className={`text-sm leading-normal tabular-nums ${amountColor}`}>
|
|
270
|
+
{formatAmount(amount)}
|
|
271
|
+
</span>
|
|
272
|
+
{!isAllCurrencies || amountCurrency ? (
|
|
273
|
+
<span className={`text-xs leading-normal ${currencyColor}`}>{amountCurrency}</span>
|
|
274
|
+
) : null}
|
|
275
|
+
</span>
|
|
276
|
+
<span className={`pl-5 text-xs leading-normal tabular-nums ${countColor}`}>{count}</span>
|
|
277
|
+
</Button>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function RadioDot({ selected }: { selected: boolean }): React.ReactElement {
|
|
282
|
+
return (
|
|
283
|
+
<span
|
|
284
|
+
className={`flex size-4 shrink-0 items-center justify-center rounded-full border ${
|
|
285
|
+
selected ? 'border-brand-violet' : 'border-input bg-card'
|
|
286
|
+
}`}
|
|
287
|
+
aria-hidden="true"
|
|
288
|
+
>
|
|
289
|
+
{selected ? <span className="size-2 rounded-full bg-brand-violet" /> : null}
|
|
290
|
+
</span>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function formatAmount(amount: number): string {
|
|
295
|
+
return new Intl.NumberFormat(undefined, {
|
|
296
|
+
style: 'decimal',
|
|
297
|
+
maximumFractionDigits: 0,
|
|
298
|
+
useGrouping: true,
|
|
299
|
+
}).format(Math.round(amount))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function formatToday(): string {
|
|
303
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
304
|
+
year: 'numeric',
|
|
305
|
+
month: 'short',
|
|
306
|
+
day: '2-digit',
|
|
307
|
+
}).format(new Date())
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default CurrencyFilterPopover
|