@open-mercato/core 0.6.3-develop.3635.1.e4a05c16e8 → 0.6.3-develop.3653.1.b8558737ae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_settings/index.js +2 -0
- package/dist/generated/entities/customer_settings/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
- package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
- package/dist/modules/customers/api/deals/route.js +255 -122
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
- package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +2 -0
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
- package/dist/modules/customers/commands/deals.js +6 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +48 -13
- package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
- package/dist/modules/customers/commands/settings.js +45 -5
- package/dist/modules/customers/commands/settings.js.map +2 -2
- package/dist/modules/customers/data/enrichers.js +130 -1
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +4 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +29 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/bulkDeals.js +150 -0
- package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js +12 -0
- package/dist/modules/customers/lib/kysely.js.map +7 -0
- package/dist/modules/customers/lib/stuckDeals.js +32 -0
- package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
- package/generated/entities/customer_settings/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
- package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
- package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
- package/src/modules/customers/api/deals/route.ts +330 -135
- package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
- package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
- package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
- package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
- package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
- package/src/modules/customers/commands/deals.ts +19 -0
- package/src/modules/customers/commands/pipeline-stages.ts +68 -13
- package/src/modules/customers/commands/settings.ts +64 -5
- package/src/modules/customers/data/enrichers.ts +209 -2
- package/src/modules/customers/data/entities.ts +4 -1
- package/src/modules/customers/data/validators.ts +36 -2
- package/src/modules/customers/i18n/de.json +244 -0
- package/src/modules/customers/i18n/en.json +244 -0
- package/src/modules/customers/i18n/es.json +244 -0
- package/src/modules/customers/i18n/pl.json +244 -0
- package/src/modules/customers/lib/bulkDeals.ts +231 -0
- package/src/modules/customers/lib/kysely.ts +60 -0
- package/src/modules/customers/lib/stuckDeals.ts +88 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
- package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
- package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
- package/src/modules/customers/workers/deals-bulk-update-stage.ts +46 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
6
|
+
import { DashedTileButton } from './DashedTileButton'
|
|
7
|
+
import { LANE_WIDTH_CLASS } from './constants'
|
|
8
|
+
|
|
9
|
+
type AddStageLaneProps = {
|
|
10
|
+
onClick: () => void
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function AddStageLane({ onClick, disabled = false }: AddStageLaneProps): React.ReactElement {
|
|
15
|
+
const t = useT()
|
|
16
|
+
const label = translateWithFallback(t, 'customers.deals.kanban.cta.newStage', 'New stage')
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<DashedTileButton
|
|
20
|
+
onClick={onClick}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
ariaLabel={label}
|
|
23
|
+
className={`${LANE_WIDTH_CLASS} flex-none flex-col rounded-xl px-3.5 py-5 text-muted-foreground hover:text-foreground min-h-[60vh]`}
|
|
24
|
+
>
|
|
25
|
+
<span className="text-3xl font-bold leading-none">+</span>
|
|
26
|
+
<span className="text-base font-semibold leading-normal">{label}</span>
|
|
27
|
+
</DashedTileButton>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default AddStageLane
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Download, Trash2, Workflow, UserCircle2, X } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
7
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
9
|
+
|
|
10
|
+
type BulkActionsBarProps = {
|
|
11
|
+
count: number
|
|
12
|
+
totalLabel: string | null
|
|
13
|
+
onChangeStage: () => void
|
|
14
|
+
onChangeOwner: () => void
|
|
15
|
+
onExportCsv: () => void
|
|
16
|
+
onDelete: () => void
|
|
17
|
+
onClear: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function BulkActionsBar({
|
|
21
|
+
count,
|
|
22
|
+
totalLabel,
|
|
23
|
+
onChangeStage,
|
|
24
|
+
onChangeOwner,
|
|
25
|
+
onExportCsv,
|
|
26
|
+
onDelete,
|
|
27
|
+
onClear,
|
|
28
|
+
}: BulkActionsBarProps): React.ReactElement | null {
|
|
29
|
+
const t = useT()
|
|
30
|
+
// Global ESC clears the selection — matches the Gmail/Asana/Notion convention. The
|
|
31
|
+
// listener is only mounted while at least one deal is selected so we don't intercept
|
|
32
|
+
// ESC anywhere else on the kanban (e.g. inside dialogs/popovers, which all stopPropagation
|
|
33
|
+
// before their own ESC handlers fire). `count` is the only thing that gates "is the bar
|
|
34
|
+
// currently rendered?", and `onClear` is the page-level handler that empties the Set.
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (count === 0) return
|
|
37
|
+
const onKey = (event: KeyboardEvent) => {
|
|
38
|
+
if (event.key !== 'Escape') return
|
|
39
|
+
// Defer if the focus is inside an editable surface — typing ESC to abort a value
|
|
40
|
+
// edit shouldn't also clear the bulk selection.
|
|
41
|
+
const active = document.activeElement as HTMLElement | null
|
|
42
|
+
if (active) {
|
|
43
|
+
const tag = active.tagName
|
|
44
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return
|
|
45
|
+
}
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
onClear()
|
|
48
|
+
}
|
|
49
|
+
document.addEventListener('keydown', onKey)
|
|
50
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
51
|
+
}, [count, onClear])
|
|
52
|
+
|
|
53
|
+
if (count === 0) return null
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
role="region"
|
|
58
|
+
aria-label={translateWithFallback(
|
|
59
|
+
t,
|
|
60
|
+
'customers.deals.kanban.bulk.aria.bar',
|
|
61
|
+
'Bulk actions',
|
|
62
|
+
)}
|
|
63
|
+
className="fixed bottom-6 left-1/2 z-toast flex w-[min(960px,calc(100%-2rem))] -translate-x-1/2 items-center gap-3 rounded-lg bg-foreground px-4 py-3 text-background shadow-xl"
|
|
64
|
+
>
|
|
65
|
+
<IconButton
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="xs"
|
|
68
|
+
onClick={onClear}
|
|
69
|
+
aria-label={translateWithFallback(
|
|
70
|
+
t,
|
|
71
|
+
'customers.deals.kanban.bulk.aria.clear',
|
|
72
|
+
'Clear selection',
|
|
73
|
+
)}
|
|
74
|
+
className="inline-flex size-6 items-center justify-center rounded-md transition-colors hover:bg-background/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-foreground"
|
|
75
|
+
>
|
|
76
|
+
<X className="size-4" aria-hidden="true" />
|
|
77
|
+
</IconButton>
|
|
78
|
+
|
|
79
|
+
<span className="inline-flex size-7 items-center justify-center rounded-md bg-background/10 text-xs font-semibold">
|
|
80
|
+
{count}
|
|
81
|
+
</span>
|
|
82
|
+
<span className="text-sm font-medium">
|
|
83
|
+
{translateWithFallback(
|
|
84
|
+
t,
|
|
85
|
+
'customers.deals.kanban.bulk.selectedCount',
|
|
86
|
+
'selected',
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
{totalLabel ? (
|
|
90
|
+
<span className="hidden text-sm text-background/80 sm:inline">
|
|
91
|
+
·{' '}
|
|
92
|
+
{translateWithFallback(
|
|
93
|
+
t,
|
|
94
|
+
'customers.deals.kanban.bulk.totalValue',
|
|
95
|
+
'Total {value}',
|
|
96
|
+
{ value: totalLabel },
|
|
97
|
+
)}
|
|
98
|
+
</span>
|
|
99
|
+
) : null}
|
|
100
|
+
|
|
101
|
+
<div className="ml-auto flex items-center gap-1">
|
|
102
|
+
<Button
|
|
103
|
+
variant="ghost"
|
|
104
|
+
size="sm"
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={onChangeStage}
|
|
107
|
+
className="inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-background/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-foreground"
|
|
108
|
+
>
|
|
109
|
+
<Workflow className="size-4" aria-hidden="true" />
|
|
110
|
+
<span>
|
|
111
|
+
{translateWithFallback(
|
|
112
|
+
t,
|
|
113
|
+
'customers.deals.kanban.bulk.changeStage',
|
|
114
|
+
'Change stage',
|
|
115
|
+
)}
|
|
116
|
+
</span>
|
|
117
|
+
</Button>
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="sm"
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={onChangeOwner}
|
|
123
|
+
className="inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-background/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-foreground"
|
|
124
|
+
>
|
|
125
|
+
<UserCircle2 className="size-4" aria-hidden="true" />
|
|
126
|
+
<span>
|
|
127
|
+
{translateWithFallback(
|
|
128
|
+
t,
|
|
129
|
+
'customers.deals.kanban.bulk.changeOwner',
|
|
130
|
+
'Change owner',
|
|
131
|
+
)}
|
|
132
|
+
</span>
|
|
133
|
+
</Button>
|
|
134
|
+
<Button
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="sm"
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onExportCsv}
|
|
139
|
+
className="inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-background/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-foreground"
|
|
140
|
+
>
|
|
141
|
+
<Download className="size-4" aria-hidden="true" />
|
|
142
|
+
<span>
|
|
143
|
+
{translateWithFallback(
|
|
144
|
+
t,
|
|
145
|
+
'customers.deals.kanban.bulk.exportCsv',
|
|
146
|
+
'Export CSV',
|
|
147
|
+
)}
|
|
148
|
+
</span>
|
|
149
|
+
</Button>
|
|
150
|
+
<Button
|
|
151
|
+
variant="destructive-ghost"
|
|
152
|
+
size="sm"
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={onDelete}
|
|
155
|
+
className="inline-flex items-center gap-1 rounded-md px-3 py-1 text-sm text-status-error-text transition-colors hover:bg-status-error-bg/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-foreground"
|
|
156
|
+
>
|
|
157
|
+
<Trash2 className="size-4" aria-hidden="true" />
|
|
158
|
+
<span>
|
|
159
|
+
{translateWithFallback(
|
|
160
|
+
t,
|
|
161
|
+
'customers.deals.kanban.bulk.delete',
|
|
162
|
+
'Delete',
|
|
163
|
+
)}
|
|
164
|
+
</span>
|
|
165
|
+
</Button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default BulkActionsBar
|
package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { UserCircle2 } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
13
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
14
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
15
|
+
import { Avatar } from '@open-mercato/ui/primitives/avatar'
|
|
16
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
17
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
19
|
+
import {
|
|
20
|
+
fetchAssignableStaffMembers,
|
|
21
|
+
type AssignableStaffMember,
|
|
22
|
+
} from '../../../../../components/detail/assignableStaff'
|
|
23
|
+
|
|
24
|
+
type ChangeOwnerDialogProps = {
|
|
25
|
+
open: boolean
|
|
26
|
+
selectedCount: number
|
|
27
|
+
isSubmitting: boolean
|
|
28
|
+
onClose: () => void
|
|
29
|
+
onConfirm: (userId: string | null) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ChangeOwnerDialog({
|
|
33
|
+
open,
|
|
34
|
+
selectedCount,
|
|
35
|
+
isSubmitting,
|
|
36
|
+
onClose,
|
|
37
|
+
onConfirm,
|
|
38
|
+
}: ChangeOwnerDialogProps): React.ReactElement {
|
|
39
|
+
const t = useT()
|
|
40
|
+
const [query, setQuery] = React.useState('')
|
|
41
|
+
const [items, setItems] = React.useState<AssignableStaffMember[]>([])
|
|
42
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
43
|
+
const [selectedUserId, setSelectedUserId] = React.useState<string | null>(null)
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (!open) return
|
|
47
|
+
setQuery('')
|
|
48
|
+
setSelectedUserId(null)
|
|
49
|
+
}, [open])
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (!open) return
|
|
53
|
+
let cancelled = false
|
|
54
|
+
const controller = new AbortController()
|
|
55
|
+
setIsLoading(true)
|
|
56
|
+
fetchAssignableStaffMembers(query, { pageSize: 50, signal: controller.signal })
|
|
57
|
+
.then((next) => {
|
|
58
|
+
if (cancelled) return
|
|
59
|
+
setItems(next)
|
|
60
|
+
if (!selectedUserId && next.length > 0) setSelectedUserId(next[0].userId)
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
if (!cancelled) setItems([])
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
if (!cancelled) setIsLoading(false)
|
|
67
|
+
})
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true
|
|
70
|
+
controller.abort()
|
|
71
|
+
}
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [open, query])
|
|
74
|
+
|
|
75
|
+
const handleConfirm = () => {
|
|
76
|
+
onConfirm(selectedUserId)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
80
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
handleConfirm()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Dialog
|
|
88
|
+
open={open}
|
|
89
|
+
onOpenChange={(next) => {
|
|
90
|
+
if (!next && !isSubmitting) onClose()
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<DialogContent className="sm:max-w-md" onKeyDown={handleKeyDown}>
|
|
94
|
+
<DialogHeader>
|
|
95
|
+
<DialogTitle className="flex items-center gap-2">
|
|
96
|
+
<UserCircle2 className="size-4" aria-hidden="true" />
|
|
97
|
+
{translateWithFallback(
|
|
98
|
+
t,
|
|
99
|
+
'customers.deals.kanban.bulk.changeOwner.title',
|
|
100
|
+
'Change owner for {count} deals',
|
|
101
|
+
{ count: selectedCount },
|
|
102
|
+
)}
|
|
103
|
+
</DialogTitle>
|
|
104
|
+
<DialogDescription>
|
|
105
|
+
{translateWithFallback(
|
|
106
|
+
t,
|
|
107
|
+
'customers.deals.kanban.bulk.changeOwner.helper',
|
|
108
|
+
'Pick a staff member who will own all selected deals.',
|
|
109
|
+
)}
|
|
110
|
+
</DialogDescription>
|
|
111
|
+
</DialogHeader>
|
|
112
|
+
|
|
113
|
+
<div className="flex flex-col gap-3">
|
|
114
|
+
<Input
|
|
115
|
+
value={query}
|
|
116
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
117
|
+
placeholder={translateWithFallback(
|
|
118
|
+
t,
|
|
119
|
+
'customers.deals.kanban.bulk.changeOwner.search',
|
|
120
|
+
'Search by name or email…',
|
|
121
|
+
)}
|
|
122
|
+
autoFocus
|
|
123
|
+
/>
|
|
124
|
+
<div className="flex max-h-72 flex-col gap-1 overflow-y-auto pr-1">
|
|
125
|
+
{isLoading ? (
|
|
126
|
+
<div className="flex justify-center py-4">
|
|
127
|
+
<Spinner />
|
|
128
|
+
</div>
|
|
129
|
+
) : items.length === 0 ? (
|
|
130
|
+
<span className="px-3 py-2 text-sm text-muted-foreground">
|
|
131
|
+
{translateWithFallback(
|
|
132
|
+
t,
|
|
133
|
+
'customers.deals.kanban.bulk.changeOwner.empty',
|
|
134
|
+
'No staff members found.',
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
) : (
|
|
138
|
+
items.map((member) => {
|
|
139
|
+
const isSelected = selectedUserId === member.userId
|
|
140
|
+
return (
|
|
141
|
+
<Button
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="default"
|
|
144
|
+
key={member.teamMemberId}
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => setSelectedUserId(member.userId)}
|
|
147
|
+
// Overrides on the Button base class:
|
|
148
|
+
// - `flex w-full` → row spans the dialog width (default Button is `inline-flex` and content-sized)
|
|
149
|
+
// - `h-auto` → fit avatar + two stacked text lines (default `h-9` clips and leaves the background only covering the first line — that's the "uneven" symptom)
|
|
150
|
+
// - `justify-start` → left-align contents (default Button is `justify-center`, which pushes the avatar+name toward the middle)
|
|
151
|
+
// - `whitespace-normal` → allow long emails to wrap instead of overflowing the button box (default Button is `whitespace-nowrap`)
|
|
152
|
+
className={`flex h-auto w-full items-center justify-start gap-3 whitespace-normal rounded-md px-3 py-2 text-left transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
|
153
|
+
isSelected ? 'bg-muted' : ''
|
|
154
|
+
}`}
|
|
155
|
+
>
|
|
156
|
+
<Avatar label={member.displayName} size="sm" />
|
|
157
|
+
<span className="flex min-w-0 flex-1 flex-col items-start">
|
|
158
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
159
|
+
{member.displayName}
|
|
160
|
+
</span>
|
|
161
|
+
{member.email ? (
|
|
162
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
163
|
+
{member.email}
|
|
164
|
+
</span>
|
|
165
|
+
) : null}
|
|
166
|
+
</span>
|
|
167
|
+
</Button>
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<DialogFooter>
|
|
175
|
+
<Button variant="outline" onClick={onClose} type="button" disabled={isSubmitting}>
|
|
176
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.cancel', 'Cancel')}
|
|
177
|
+
</Button>
|
|
178
|
+
<Button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={handleConfirm}
|
|
181
|
+
disabled={!selectedUserId || isSubmitting}
|
|
182
|
+
>
|
|
183
|
+
{translateWithFallback(
|
|
184
|
+
t,
|
|
185
|
+
'customers.deals.kanban.bulk.changeOwner.confirm',
|
|
186
|
+
'Reassign {count} deals',
|
|
187
|
+
{ count: selectedCount },
|
|
188
|
+
)}
|
|
189
|
+
</Button>
|
|
190
|
+
</DialogFooter>
|
|
191
|
+
</DialogContent>
|
|
192
|
+
</Dialog>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default ChangeOwnerDialog
|
package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Workflow } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
13
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
14
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
16
|
+
|
|
17
|
+
export type ChangeStageOption = {
|
|
18
|
+
id: string
|
|
19
|
+
label: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ChangeStageDialogProps = {
|
|
23
|
+
open: boolean
|
|
24
|
+
selectedCount: number
|
|
25
|
+
pipelineName: string
|
|
26
|
+
stages: ChangeStageOption[]
|
|
27
|
+
isSubmitting: boolean
|
|
28
|
+
onClose: () => void
|
|
29
|
+
onConfirm: (stageId: string) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ChangeStageDialog({
|
|
33
|
+
open,
|
|
34
|
+
selectedCount,
|
|
35
|
+
pipelineName,
|
|
36
|
+
stages,
|
|
37
|
+
isSubmitting,
|
|
38
|
+
onClose,
|
|
39
|
+
onConfirm,
|
|
40
|
+
}: ChangeStageDialogProps): React.ReactElement {
|
|
41
|
+
const t = useT()
|
|
42
|
+
const [draftStageId, setDraftStageId] = React.useState<string | null>(stages[0]?.id ?? null)
|
|
43
|
+
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (open) setDraftStageId(stages[0]?.id ?? null)
|
|
46
|
+
}, [open, stages])
|
|
47
|
+
|
|
48
|
+
const handleConfirm = () => {
|
|
49
|
+
if (draftStageId) onConfirm(draftStageId)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
53
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
54
|
+
event.preventDefault()
|
|
55
|
+
handleConfirm()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Dialog
|
|
61
|
+
open={open}
|
|
62
|
+
onOpenChange={(next) => {
|
|
63
|
+
if (!next && !isSubmitting) onClose()
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<DialogContent className="sm:max-w-md" onKeyDown={handleKeyDown}>
|
|
67
|
+
<DialogHeader>
|
|
68
|
+
<DialogTitle className="flex items-center gap-2">
|
|
69
|
+
<Workflow className="size-4" aria-hidden="true" />
|
|
70
|
+
{translateWithFallback(
|
|
71
|
+
t,
|
|
72
|
+
'customers.deals.kanban.bulk.changeStage.title',
|
|
73
|
+
'Change stage for {count} deals',
|
|
74
|
+
{ count: selectedCount },
|
|
75
|
+
)}
|
|
76
|
+
</DialogTitle>
|
|
77
|
+
<DialogDescription>
|
|
78
|
+
{translateWithFallback(
|
|
79
|
+
t,
|
|
80
|
+
'customers.deals.kanban.bulk.changeStage.context',
|
|
81
|
+
'Pipeline: {pipeline}',
|
|
82
|
+
{ pipeline: pipelineName },
|
|
83
|
+
)}
|
|
84
|
+
</DialogDescription>
|
|
85
|
+
</DialogHeader>
|
|
86
|
+
|
|
87
|
+
<div className="flex flex-col gap-1 py-1">
|
|
88
|
+
{stages.map((stage) => {
|
|
89
|
+
const isSelected = draftStageId === stage.id
|
|
90
|
+
return (
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
key={stage.id}
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => setDraftStageId(stage.id)}
|
|
96
|
+
className={`flex w-full items-center justify-between rounded-md px-3 py-2 text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
|
97
|
+
isSelected ? 'bg-muted' : ''
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<span className="font-medium text-foreground">{stage.label}</span>
|
|
101
|
+
<span
|
|
102
|
+
className={`inline-flex size-4 items-center justify-center rounded-full border ${
|
|
103
|
+
isSelected ? 'border-primary bg-primary' : 'border-input'
|
|
104
|
+
}`}
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
>
|
|
107
|
+
{isSelected ? (
|
|
108
|
+
<span className="size-1.5 rounded-full bg-primary-foreground" />
|
|
109
|
+
) : null}
|
|
110
|
+
</span>
|
|
111
|
+
</Button>
|
|
112
|
+
)
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<DialogFooter>
|
|
117
|
+
<Button variant="outline" onClick={onClose} type="button" disabled={isSubmitting}>
|
|
118
|
+
{translateWithFallback(t, 'customers.deals.kanban.filter.cancel', 'Cancel')}
|
|
119
|
+
</Button>
|
|
120
|
+
<Button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={handleConfirm}
|
|
123
|
+
disabled={!draftStageId || isSubmitting || stages.length === 0}
|
|
124
|
+
>
|
|
125
|
+
{translateWithFallback(
|
|
126
|
+
t,
|
|
127
|
+
'customers.deals.kanban.bulk.changeStage.confirm',
|
|
128
|
+
'Move {count} deals',
|
|
129
|
+
{ count: selectedCount },
|
|
130
|
+
)}
|
|
131
|
+
</Button>
|
|
132
|
+
</DialogFooter>
|
|
133
|
+
</DialogContent>
|
|
134
|
+
</Dialog>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default ChangeStageDialog
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { ChevronDown } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
|
|
7
|
+
export type ChipButtonProps = {
|
|
8
|
+
label?: string
|
|
9
|
+
value: string
|
|
10
|
+
active?: boolean
|
|
11
|
+
ariaLabel?: string
|
|
12
|
+
onClick?: () => void
|
|
13
|
+
/** Show the chevron-down affordance — true for popover-backed chips, false for plain chips like "+ More" */
|
|
14
|
+
withChevron?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ChipButton = React.forwardRef<HTMLButtonElement, ChipButtonProps>(
|
|
18
|
+
function ChipButton(
|
|
19
|
+
{ label, value, active = false, ariaLabel, onClick, withChevron = true },
|
|
20
|
+
ref,
|
|
21
|
+
) {
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
variant="ghost"
|
|
25
|
+
size="sm"
|
|
26
|
+
ref={ref}
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
aria-label={ariaLabel ?? (label ? `${label}: ${value}` : value)}
|
|
30
|
+
aria-pressed={active}
|
|
31
|
+
className={`inline-flex items-center gap-1.5 rounded-sm border border-border bg-card px-2.5 py-1.5 text-xs leading-normal transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
|
|
32
|
+
active ? 'border-foreground/40 bg-muted text-foreground' : 'text-foreground'
|
|
33
|
+
}`}
|
|
34
|
+
>
|
|
35
|
+
{label ? <span className="font-normal text-muted-foreground">{label}:</span> : null}
|
|
36
|
+
<span className={label ? 'font-semibold text-foreground' : 'font-normal text-foreground'}>
|
|
37
|
+
{value}
|
|
38
|
+
</span>
|
|
39
|
+
{withChevron ? (
|
|
40
|
+
<ChevronDown className="size-3 text-muted-foreground" aria-hidden="true" />
|
|
41
|
+
) : null}
|
|
42
|
+
</Button>
|
|
43
|
+
)
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export default ChipButton
|