@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.
Files changed (160) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_settings/index.js +2 -0
  3. package/dist/generated/entities/customer_settings/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
  7. package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
  8. package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
  9. package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
  10. package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
  11. package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
  12. package/dist/modules/customers/api/deals/route.js +255 -122
  13. package/dist/modules/customers/api/deals/route.js.map +2 -2
  14. package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
  15. package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
  16. package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
  17. package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
  18. package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
  19. package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
  20. package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
  21. package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
  22. package/dist/modules/customers/backend/customers/deals/page.js +2 -0
  23. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  24. package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
  25. package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
  26. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
  27. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
  28. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
  29. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
  30. package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
  31. package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
  32. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
  33. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
  34. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
  35. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
  36. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
  37. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
  38. package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
  39. package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
  40. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
  41. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
  42. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
  43. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
  44. package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
  45. package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
  46. package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
  47. package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
  48. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
  49. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
  50. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
  51. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
  52. package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
  53. package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
  54. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
  55. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
  56. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
  57. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
  58. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
  59. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
  60. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
  61. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
  62. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
  63. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
  64. package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
  65. package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
  66. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
  67. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
  68. package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
  69. package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
  70. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
  71. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
  72. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
  73. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
  74. package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
  75. package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
  76. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
  77. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
  78. package/dist/modules/customers/commands/deals.js +6 -0
  79. package/dist/modules/customers/commands/deals.js.map +2 -2
  80. package/dist/modules/customers/commands/pipeline-stages.js +48 -13
  81. package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
  82. package/dist/modules/customers/commands/settings.js +45 -5
  83. package/dist/modules/customers/commands/settings.js.map +2 -2
  84. package/dist/modules/customers/data/enrichers.js +130 -1
  85. package/dist/modules/customers/data/enrichers.js.map +2 -2
  86. package/dist/modules/customers/data/entities.js +4 -0
  87. package/dist/modules/customers/data/entities.js.map +2 -2
  88. package/dist/modules/customers/data/validators.js +29 -2
  89. package/dist/modules/customers/data/validators.js.map +2 -2
  90. package/dist/modules/customers/lib/bulkDeals.js +150 -0
  91. package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
  92. package/dist/modules/customers/lib/kysely.js +12 -0
  93. package/dist/modules/customers/lib/kysely.js.map +7 -0
  94. package/dist/modules/customers/lib/stuckDeals.js +32 -0
  95. package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
  96. package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
  97. package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
  98. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
  99. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
  100. package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
  101. package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
  102. package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
  103. package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
  104. package/generated/entities/customer_settings/index.ts +1 -0
  105. package/generated/entity-fields-registry.ts +1 -0
  106. package/package.json +7 -7
  107. package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
  108. package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
  109. package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
  110. package/src/modules/customers/api/deals/route.ts +330 -135
  111. package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
  112. package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
  113. package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
  114. package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
  115. package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
  116. package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
  117. package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
  118. package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
  119. package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
  120. package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
  121. package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
  122. package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
  123. package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
  124. package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
  125. package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
  126. package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
  127. package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
  128. package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
  129. package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
  130. package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
  131. package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
  132. package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
  133. package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
  134. package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
  135. package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
  136. package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
  137. package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
  138. package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
  139. package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
  140. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
  141. package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
  142. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
  143. package/src/modules/customers/commands/deals.ts +19 -0
  144. package/src/modules/customers/commands/pipeline-stages.ts +68 -13
  145. package/src/modules/customers/commands/settings.ts +64 -5
  146. package/src/modules/customers/data/enrichers.ts +209 -2
  147. package/src/modules/customers/data/entities.ts +4 -1
  148. package/src/modules/customers/data/validators.ts +36 -2
  149. package/src/modules/customers/i18n/de.json +244 -0
  150. package/src/modules/customers/i18n/en.json +244 -0
  151. package/src/modules/customers/i18n/es.json +244 -0
  152. package/src/modules/customers/i18n/pl.json +244 -0
  153. package/src/modules/customers/lib/bulkDeals.ts +231 -0
  154. package/src/modules/customers/lib/kysely.ts +60 -0
  155. package/src/modules/customers/lib/stuckDeals.ts +88 -0
  156. package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
  157. package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
  158. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
  159. package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
  160. 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
@@ -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]'