@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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Tag } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverTrigger,
|
|
9
|
+
} from '@open-mercato/ui/primitives/popover'
|
|
10
|
+
import { Radio, RadioGroup } from '@open-mercato/ui/primitives/radio'
|
|
11
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
13
|
+
import { ChipButton } from './ChipButton'
|
|
14
|
+
import { FilterPopoverShell } from './FilterPopoverShell'
|
|
15
|
+
|
|
16
|
+
export type SortOption =
|
|
17
|
+
| 'updated_desc'
|
|
18
|
+
| 'updated_asc'
|
|
19
|
+
| 'created_desc'
|
|
20
|
+
| 'value_desc'
|
|
21
|
+
| 'value_asc'
|
|
22
|
+
| 'probability_desc'
|
|
23
|
+
| 'close_asc'
|
|
24
|
+
| 'owner_asc'
|
|
25
|
+
|
|
26
|
+
export const SORT_LABEL_KEYS: Record<SortOption, { key: string; fallback: string }> = {
|
|
27
|
+
updated_desc: { key: 'customers.deals.kanban.sort.option.updatedNewest', fallback: 'Updated (newest)' },
|
|
28
|
+
updated_asc: { key: 'customers.deals.kanban.sort.option.updatedOldest', fallback: 'Updated (oldest)' },
|
|
29
|
+
created_desc: { key: 'customers.deals.kanban.sort.option.createdNewest', fallback: 'Created (newest)' },
|
|
30
|
+
value_desc: { key: 'customers.deals.kanban.sort.option.valueHigh', fallback: 'Value (high to low)' },
|
|
31
|
+
value_asc: { key: 'customers.deals.kanban.sort.option.valueLow', fallback: 'Value (low to high)' },
|
|
32
|
+
probability_desc: {
|
|
33
|
+
key: 'customers.deals.kanban.sort.option.probabilityHigh',
|
|
34
|
+
fallback: 'Probability (high to low)',
|
|
35
|
+
},
|
|
36
|
+
close_asc: { key: 'customers.deals.kanban.sort.option.closeSoonest', fallback: 'Close date (soonest)' },
|
|
37
|
+
owner_asc: { key: 'customers.deals.kanban.sort.option.ownerAsc', fallback: 'Owner (A → Z)' },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SORT_ORDER: SortOption[] = [
|
|
41
|
+
'updated_desc',
|
|
42
|
+
'updated_asc',
|
|
43
|
+
'created_desc',
|
|
44
|
+
'value_desc',
|
|
45
|
+
'value_asc',
|
|
46
|
+
'probability_desc',
|
|
47
|
+
'close_asc',
|
|
48
|
+
'owner_asc',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const DEFAULT_SORT: SortOption = 'updated_desc'
|
|
52
|
+
|
|
53
|
+
type SortByPopoverProps = {
|
|
54
|
+
value: SortOption
|
|
55
|
+
onApply: (next: SortOption) => void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function SortByPopover({ value, onApply }: SortByPopoverProps): React.ReactElement {
|
|
59
|
+
const t = useT()
|
|
60
|
+
const [open, setOpen] = React.useState(false)
|
|
61
|
+
const [draft, setDraft] = React.useState<SortOption>(value)
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (open) setDraft(value)
|
|
65
|
+
}, [open, value])
|
|
66
|
+
|
|
67
|
+
const chipLabel = translateWithFallback(t, 'customers.deals.kanban.sort.label', 'Sort')
|
|
68
|
+
const chipValue = translateWithFallback(t, SORT_LABEL_KEYS[value].key, SORT_LABEL_KEYS[value].fallback)
|
|
69
|
+
|
|
70
|
+
const handleApply = () => {
|
|
71
|
+
onApply(draft)
|
|
72
|
+
setOpen(false)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Cmd/Ctrl+Enter from anywhere inside the popover confirms — parity with the dialog
|
|
76
|
+
// primary-action shortcut (`AGENTS.md` UI Interaction rules).
|
|
77
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
78
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
79
|
+
event.preventDefault()
|
|
80
|
+
handleApply()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const footerLeft = (
|
|
85
|
+
<span>
|
|
86
|
+
{translateWithFallback(
|
|
87
|
+
t,
|
|
88
|
+
'customers.deals.kanban.sort.defaultHint',
|
|
89
|
+
'Default: {label}',
|
|
90
|
+
{
|
|
91
|
+
label: translateWithFallback(
|
|
92
|
+
t,
|
|
93
|
+
SORT_LABEL_KEYS[DEFAULT_SORT].key,
|
|
94
|
+
SORT_LABEL_KEYS[DEFAULT_SORT].fallback,
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
)}
|
|
98
|
+
</span>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
103
|
+
<PopoverTrigger asChild>
|
|
104
|
+
<ChipButton label={chipLabel} value={chipValue} active={value !== DEFAULT_SORT} />
|
|
105
|
+
</PopoverTrigger>
|
|
106
|
+
<PopoverContent
|
|
107
|
+
className="w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl"
|
|
108
|
+
align="end"
|
|
109
|
+
onKeyDown={handleKeyDown}
|
|
110
|
+
>
|
|
111
|
+
<FilterPopoverShell
|
|
112
|
+
title={translateWithFallback(t, 'customers.deals.kanban.sort.title', 'Sort by')}
|
|
113
|
+
leadingIcon={<Tag className="size-4" />}
|
|
114
|
+
onClose={() => setOpen(false)}
|
|
115
|
+
onCancel={() => setOpen(false)}
|
|
116
|
+
onApply={handleApply}
|
|
117
|
+
footerLeft={footerLeft}
|
|
118
|
+
>
|
|
119
|
+
<RadioGroup
|
|
120
|
+
value={draft}
|
|
121
|
+
onValueChange={(next) => setDraft(next as SortOption)}
|
|
122
|
+
className="gap-0"
|
|
123
|
+
>
|
|
124
|
+
{SORT_ORDER.map((option) => {
|
|
125
|
+
const isSelected = draft === option
|
|
126
|
+
const isDefault = option === DEFAULT_SORT
|
|
127
|
+
const rowClass = isSelected ? 'bg-muted' : 'bg-card'
|
|
128
|
+
const inputId = `sort-by-${option}`
|
|
129
|
+
return (
|
|
130
|
+
<label
|
|
131
|
+
key={option}
|
|
132
|
+
htmlFor={inputId}
|
|
133
|
+
className={`flex w-full cursor-pointer items-center gap-3 rounded-md px-4 py-2.5 text-left transition-colors hover:bg-muted ${rowClass}`}
|
|
134
|
+
>
|
|
135
|
+
<Radio id={inputId} value={option} />
|
|
136
|
+
<div className="flex min-w-0 flex-1 flex-col gap-px">
|
|
137
|
+
<span
|
|
138
|
+
className={`text-sm leading-normal text-foreground ${
|
|
139
|
+
isSelected ? 'font-semibold' : 'font-normal'
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
{translateWithFallback(
|
|
143
|
+
t,
|
|
144
|
+
SORT_LABEL_KEYS[option].key,
|
|
145
|
+
SORT_LABEL_KEYS[option].fallback,
|
|
146
|
+
)}
|
|
147
|
+
</span>
|
|
148
|
+
{isDefault && isSelected ? (
|
|
149
|
+
<span className="text-xs font-normal leading-normal text-muted-foreground">
|
|
150
|
+
{translateWithFallback(t, 'customers.deals.kanban.sort.default', 'Default')}
|
|
151
|
+
</span>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
</label>
|
|
155
|
+
)
|
|
156
|
+
})}
|
|
157
|
+
</RadioGroup>
|
|
158
|
+
</FilterPopoverShell>
|
|
159
|
+
</PopoverContent>
|
|
160
|
+
</Popover>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default SortByPopover
|
package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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 {
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverContent,
|
|
9
|
+
PopoverTrigger,
|
|
10
|
+
} from '@open-mercato/ui/primitives/popover'
|
|
11
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
13
|
+
import { ChipButton } from './ChipButton'
|
|
14
|
+
import { FilterPopoverShell } from './FilterPopoverShell'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Filter options exposed to the operator.
|
|
18
|
+
*
|
|
19
|
+
* The deal `status` column accepts the historical 4-value enum (`open` | `closed` | `win` |
|
|
20
|
+
* `loose`), but the codebase only ever **writes** `loose` for a lost deal — `closed` was a
|
|
21
|
+
* latent unused state from an earlier iteration. Exposing a separate "Lost (closed)" filter
|
|
22
|
+
* option therefore filtered to nothing and confused operators. We display a single "Lost"
|
|
23
|
+
* choice and intentionally map it to the canonical wire value `loose` so the filter matches
|
|
24
|
+
* what the rest of the app actually persists. (Renaming the column / migrating the data to
|
|
25
|
+
* `lost` is a deeper data-model change tracked separately.)
|
|
26
|
+
*/
|
|
27
|
+
const STATUS_OPTIONS: Array<{
|
|
28
|
+
value: string
|
|
29
|
+
labelKey: string
|
|
30
|
+
labelFallback: string
|
|
31
|
+
/** Tone selects the small ●-dot color on the pill (per Figma: green / amber / gray) */
|
|
32
|
+
dotClass: string
|
|
33
|
+
}> = [
|
|
34
|
+
{
|
|
35
|
+
value: 'open',
|
|
36
|
+
labelKey: 'customers.deals.kanban.filter.status.open',
|
|
37
|
+
labelFallback: 'Open',
|
|
38
|
+
dotClass: 'bg-status-success-icon',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: 'win',
|
|
42
|
+
labelKey: 'customers.deals.kanban.filter.status.won',
|
|
43
|
+
labelFallback: 'Won',
|
|
44
|
+
dotClass: 'bg-status-warning-icon',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: 'loose',
|
|
48
|
+
labelKey: 'customers.deals.kanban.filter.status.lost',
|
|
49
|
+
labelFallback: 'Lost',
|
|
50
|
+
dotClass: 'bg-status-neutral-icon',
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
type StatusFilterPopoverProps = {
|
|
55
|
+
values: string[]
|
|
56
|
+
onApply: (next: string[]) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function StatusFilterPopover({ values, onApply }: StatusFilterPopoverProps): React.ReactElement {
|
|
60
|
+
const t = useT()
|
|
61
|
+
const [open, setOpen] = React.useState(false)
|
|
62
|
+
const [draft, setDraft] = React.useState<string[]>(values)
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (open) setDraft(values)
|
|
66
|
+
}, [open, values])
|
|
67
|
+
|
|
68
|
+
const chipLabel = translateWithFallback(t, 'customers.deals.kanban.filter.status', 'Status')
|
|
69
|
+
const chipValue =
|
|
70
|
+
values.length === 0
|
|
71
|
+
? translateWithFallback(t, 'customers.deals.kanban.filter.all', 'All')
|
|
72
|
+
: values
|
|
73
|
+
.map((value) => {
|
|
74
|
+
const option = STATUS_OPTIONS.find((entry) => entry.value === value)
|
|
75
|
+
return option
|
|
76
|
+
? translateWithFallback(t, option.labelKey, option.labelFallback)
|
|
77
|
+
: value
|
|
78
|
+
})
|
|
79
|
+
.join(', ')
|
|
80
|
+
|
|
81
|
+
const toggleDraft = (value: string) => {
|
|
82
|
+
setDraft((prev) =>
|
|
83
|
+
prev.includes(value) ? prev.filter((entry) => entry !== value) : [...prev, value],
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handleApply = () => {
|
|
88
|
+
onApply(draft)
|
|
89
|
+
setOpen(false)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Cmd/Ctrl+Enter from anywhere inside the popover confirms — parity with the dialog
|
|
93
|
+
// primary-action shortcut (`AGENTS.md` UI Interaction rules).
|
|
94
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
95
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
handleApply()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
103
|
+
<PopoverTrigger asChild>
|
|
104
|
+
<ChipButton label={chipLabel} value={chipValue} active={values.length > 0} />
|
|
105
|
+
</PopoverTrigger>
|
|
106
|
+
<PopoverContent
|
|
107
|
+
className="w-96 rounded-2xl border-border bg-transparent p-0 shadow-xl"
|
|
108
|
+
align="start"
|
|
109
|
+
onKeyDown={handleKeyDown}
|
|
110
|
+
>
|
|
111
|
+
<FilterPopoverShell
|
|
112
|
+
title={
|
|
113
|
+
<>
|
|
114
|
+
<span className="font-bold">
|
|
115
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.status.title.label', 'Filter : ')}
|
|
116
|
+
</span>
|
|
117
|
+
<span className="font-normal">
|
|
118
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.status', 'Status')}
|
|
119
|
+
</span>
|
|
120
|
+
</>
|
|
121
|
+
}
|
|
122
|
+
onClose={() => setOpen(false)}
|
|
123
|
+
onCancel={() => setOpen(false)}
|
|
124
|
+
onApply={handleApply}
|
|
125
|
+
footerLeft={
|
|
126
|
+
<span>
|
|
127
|
+
{draft.length}{' '}
|
|
128
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.selected', 'selected')}
|
|
129
|
+
</span>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<span className="text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground">
|
|
133
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.status', 'Status')}
|
|
134
|
+
</span>
|
|
135
|
+
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-2">
|
|
136
|
+
{STATUS_OPTIONS.map((option) => {
|
|
137
|
+
const isSelected = draft.includes(option.value)
|
|
138
|
+
const label = translateWithFallback(t, option.labelKey, option.labelFallback)
|
|
139
|
+
return (
|
|
140
|
+
<Button
|
|
141
|
+
key={option.value}
|
|
142
|
+
type="button"
|
|
143
|
+
variant="ghost"
|
|
144
|
+
size="2xs"
|
|
145
|
+
onClick={() => toggleDraft(option.value)}
|
|
146
|
+
aria-pressed={isSelected}
|
|
147
|
+
className={`gap-1.5 rounded-full px-2.5 py-1.5 text-xs leading-normal ${
|
|
148
|
+
isSelected
|
|
149
|
+
? 'bg-muted font-semibold text-foreground'
|
|
150
|
+
: 'border border-border bg-card font-normal text-muted-foreground hover:bg-muted'
|
|
151
|
+
}`}
|
|
152
|
+
>
|
|
153
|
+
<span
|
|
154
|
+
className={`inline-block size-2 shrink-0 rounded-full ${option.dotClass}`}
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
/>
|
|
157
|
+
<span>{label}</span>
|
|
158
|
+
{isSelected ? (
|
|
159
|
+
<X className="size-2.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
160
|
+
) : null}
|
|
161
|
+
</Button>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
</FilterPopoverShell>
|
|
166
|
+
</PopoverContent>
|
|
167
|
+
</Popover>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default StatusFilterPopover
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
7
|
+
|
|
8
|
+
export type KanbanView = 'kanban' | 'list'
|
|
9
|
+
|
|
10
|
+
type ViewTabsRowProps = {
|
|
11
|
+
active: KanbanView
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VIEW_KANBAN_HREF = '/backend/customers/deals/pipeline'
|
|
16
|
+
const VIEW_LIST_HREF = '/backend/customers/deals'
|
|
17
|
+
|
|
18
|
+
export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.ReactElement {
|
|
19
|
+
const t = useT()
|
|
20
|
+
const labels = {
|
|
21
|
+
kanban: translateWithFallback(t, 'customers.deals.kanban.view.kanban', 'Kanban'),
|
|
22
|
+
list: translateWithFallback(t, 'customers.deals.kanban.view.list', 'List'),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const baseTab =
|
|
26
|
+
'inline-flex items-center px-3.5 py-2.5 text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
|
|
27
|
+
const activeTab = 'border-b-2 border-foreground font-semibold text-foreground'
|
|
28
|
+
const inactiveTab = 'border-b-2 border-transparent font-normal text-muted-foreground hover:text-foreground'
|
|
29
|
+
|
|
30
|
+
// Renders the active tab as a non-navigating `<span>` and the inactive tab as a `<Link>`,
|
|
31
|
+
// so the user can always round-trip between kanban and list from either page. Previously
|
|
32
|
+
// the kanban tab was a hardcoded `<span>` because the row was only rendered on the kanban
|
|
33
|
+
// page — adding it to the list page (item 7 of the SPEC-048 UX review) requires it to
|
|
34
|
+
// navigate back to /pipeline.
|
|
35
|
+
const isKanbanActive = active === 'kanban'
|
|
36
|
+
const isListActive = active === 'list'
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
role="tablist"
|
|
41
|
+
aria-label={translateWithFallback(t, 'customers.deals.kanban.view.tablistLabel', 'Deals views')}
|
|
42
|
+
className={`flex items-end gap-1 border-b border-border ${className ?? ''}`.trim()}
|
|
43
|
+
>
|
|
44
|
+
{isKanbanActive ? (
|
|
45
|
+
<span
|
|
46
|
+
role="tab"
|
|
47
|
+
aria-selected={true}
|
|
48
|
+
className={`${baseTab} ${activeTab}`}
|
|
49
|
+
>
|
|
50
|
+
{labels.kanban}
|
|
51
|
+
</span>
|
|
52
|
+
) : (
|
|
53
|
+
<Link
|
|
54
|
+
href={VIEW_KANBAN_HREF}
|
|
55
|
+
role="tab"
|
|
56
|
+
aria-selected={false}
|
|
57
|
+
className={`${baseTab} ${inactiveTab}`}
|
|
58
|
+
>
|
|
59
|
+
{labels.kanban}
|
|
60
|
+
</Link>
|
|
61
|
+
)}
|
|
62
|
+
{isListActive ? (
|
|
63
|
+
<span
|
|
64
|
+
role="tab"
|
|
65
|
+
aria-selected={true}
|
|
66
|
+
className={`${baseTab} ${activeTab}`}
|
|
67
|
+
>
|
|
68
|
+
{labels.list}
|
|
69
|
+
</span>
|
|
70
|
+
) : (
|
|
71
|
+
<Link
|
|
72
|
+
href={VIEW_LIST_HREF}
|
|
73
|
+
role="tab"
|
|
74
|
+
aria-selected={false}
|
|
75
|
+
className={`${baseTab} ${inactiveTab}`}
|
|
76
|
+
>
|
|
77
|
+
{labels.list}
|
|
78
|
+
</Link>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default ViewTabsRow
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default kanban lane width in pixels (matches SPEC-048 Figma node 982:335). Operators can
|
|
3
|
+
* override per-lane via the drag-resize handle; this constant is the fallback used when no
|
|
4
|
+
* override is recorded.
|
|
5
|
+
*
|
|
6
|
+
* Centralised here so the value can't drift between the Lane wrapper, the AddStageLane CTA,
|
|
7
|
+
* and the page-level DragOverlay — three places that previously hand-wrote `w-[308px]` and
|
|
8
|
+
* `style={{ width: '308px' }}` independently.
|
|
9
|
+
*/
|
|
10
|
+
export const LANE_WIDTH_PX = 308
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tailwind utility class encoding `LANE_WIDTH_PX`. Use this in `className` templates;
|
|
14
|
+
* use `LANE_WIDTH_PX` in inline `style` blocks where the width is genuinely dynamic.
|
|
15
|
+
*/
|
|
16
|
+
export const LANE_WIDTH_CLASS = 'w-[308px]'
|