@open-mercato/core 0.10.1-develop.7874.1.67e097d23a → 0.10.1-develop.7883.1.44ac2125f5

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 (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/lib/transferClient.js +72 -0
  3. package/dist/modules/attachments/lib/transferClient.js.map +7 -0
  4. package/dist/modules/attachments/lib/transferLimits.js +5 -0
  5. package/dist/modules/attachments/lib/transferLimits.js.map +7 -0
  6. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +23 -26
  7. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  8. package/dist/modules/catalog/backend/catalog/products/create/page.js +31 -23
  9. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  10. package/dist/modules/catalog/components/products/ProductsDataTable.js +7 -8
  11. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  12. package/dist/modules/catalog/components/products/mediaTransfer.js +27 -0
  13. package/dist/modules/catalog/components/products/mediaTransfer.js.map +7 -0
  14. package/dist/modules/customer_groups/backend/customer-groups/page.js +5 -3
  15. package/dist/modules/customer_groups/backend/customer-groups/page.js.map +2 -2
  16. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +4 -4
  17. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +2 -2
  18. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +5 -2
  19. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +43 -31
  21. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  22. package/dist/modules/push_notifications/backend/push_notifications/page.js +8 -13
  23. package/dist/modules/push_notifications/backend/push_notifications/page.js.map +2 -2
  24. package/package.json +7 -7
  25. package/src/modules/attachments/lib/transferClient.ts +87 -0
  26. package/src/modules/attachments/lib/transferLimits.ts +1 -0
  27. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +23 -32
  28. package/src/modules/catalog/backend/catalog/products/create/page.tsx +31 -28
  29. package/src/modules/catalog/components/products/ProductsDataTable.tsx +7 -8
  30. package/src/modules/catalog/components/products/mediaTransfer.ts +40 -0
  31. package/src/modules/catalog/i18n/de.json +2 -0
  32. package/src/modules/catalog/i18n/en.json +2 -0
  33. package/src/modules/catalog/i18n/es.json +2 -0
  34. package/src/modules/catalog/i18n/ko.json +2 -0
  35. package/src/modules/catalog/i18n/pl.json +2 -0
  36. package/src/modules/customer_groups/backend/customer-groups/page.tsx +5 -3
  37. package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +4 -4
  38. package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +6 -2
  39. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +45 -32
  40. package/src/modules/push_notifications/backend/push_notifications/page.tsx +8 -13
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useDroppable } from '@dnd-kit/core'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { toneToDotClass } from './toneClasses'\nimport { DealCard, type DealCardData } from './DealCard'\nimport { DashedTileButton } from './DashedTileButton'\nimport { LANE_WIDTH_PX } from './constants'\nimport { LaneCurrencyBreakdown } from './LaneCurrencyBreakdown'\nimport { LaneResizeHandle } from './LaneResizeHandle'\n\nexport type LaneStage = {\n id: string\n label: string\n tone: FilterOptionTone | null\n}\n\nexport type LaneAggregateProp = {\n count: number\n totalInBaseCurrency: number\n byCurrency: Array<{ currency: string; total: number; count: number }>\n baseCurrencyCode: string | null\n /** `true` when every currency in `byCurrency` was convertible to the base currency. */\n convertedAll: boolean\n /** Currencies present in `byCurrency` that lack an FX rate to base \u2014 excluded from totals. */\n missingRateCurrencies: string[]\n}\n\ntype LaneProps = {\n stage: LaneStage\n deals: DealCardData[]\n /** Server-side aggregate for accurate counts/totals across all deals in this stage, not just loaded ones */\n aggregate?: LaneAggregateProp | null\n selectedDealIds: Set<string>\n buildMenuItems: (deal: DealCardData) => RowActionItem[]\n activeDragDealId?: string | null\n isLoadingMore?: boolean\n /** Pixel width override (when the user has resized the column). Falls back to the default class. */\n width?: number | null\n onToggleSelect: (dealId: string) => void\n onComposeActivity: (dealId: string, type: 'call' | 'email' | 'note') => void\n onOpenDetail: (dealId: string) => void\n onQuickAddClick: (stageId: string) => void\n onLoadMore?: (stageId: string) => void\n /** Drag-resize callbacks. When provided, a vertical drag handle is rendered on the lane edge. */\n onResize?: (stageId: string, deltaPx: number) => void\n onResizeEnd?: (stageId: string) => void\n onResetWidth?: (stageId: string) => void\n}\n\n// 4px color bar tone \u2014 saturated icon tokens shared with the status filter pills\n// (toneClasses.ts) so the lane bar and the filter dots stay in lockstep (#5107 review).\nfunction getAccentClass(tone: FilterOptionTone | null): string {\n return toneToDotClass(tone, 'bg-border')\n}\n\n// Count pill bg uses very-light status background, text uses status text color\nconst COUNT_BADGE_TONE_CLASS: Record<FilterOptionTone, string> = {\n success: 'bg-status-success-bg text-status-success-text',\n error: 'bg-status-error-bg text-status-error-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n info: 'bg-status-info-bg text-status-info-text',\n neutral: 'bg-status-neutral-bg text-status-neutral-text',\n brand: 'bg-brand-violet/14 text-brand-violet',\n pink: 'bg-status-pink-bg text-status-pink-text',\n}\n\nfunction getCountBadgeClass(tone: FilterOptionTone | null): string {\n if (tone && tone in COUNT_BADGE_TONE_CLASS) return COUNT_BADGE_TONE_CLASS[tone]\n return 'bg-muted text-muted-foreground'\n}\n\nfunction compactCurrency(amount: number): string {\n if (amount >= 1_000_000) {\n const value = amount / 1_000_000\n return value >= 100 ? `${Math.round(value)}M` : `${value.toFixed(1).replace(/\\.0$/, '')}M`\n }\n if (amount >= 1_000) {\n const value = amount / 1_000\n return value >= 100 ? `${Math.round(value)}k` : `${value.toFixed(1).replace(/\\.0$/, '')}k`\n }\n return String(Math.round(amount))\n}\n\ntype CurrencyRow = { currency: string; total: number; count: number }\n\ntype LaneStats = {\n count: number\n /** Headline amount and the currency it is denominated in (largest single currency, raw, never converted). */\n primaryAmount: number\n primaryCurrency: string | null\n /** Per-currency rows sorted desc by total \u2014 the popover renders these one per line. */\n rows: CurrencyRow[]\n /** Sum of `rows` converted to base currency (zero when no base is configured / nothing converted). */\n totalInBaseCurrency: number\n baseCurrencyCode: string | null\n convertedAll: boolean\n missingRateCurrencies: string[]\n}\n\n/**\n * Build display stats.\n * - If a server aggregate is provided, prefer it (covers all deals, converts to tenant base currency).\n * - Otherwise fall back to summing loaded deals (used before aggregate query loads).\n *\n * The headline currency on the lane header is intentionally the LARGEST raw currency\n * (not the base-currency converted total) because the converted total can be misleading\n * when FX rates are missing \u2014 operators see a smaller number than the headline suggests.\n * The base-currency converted total is reserved for the breakdown popover footer, where\n * a partial-conversion indicator can disclose what's excluded.\n */\nfunction buildLaneStats(deals: DealCardData[], aggregate?: LaneAggregateProp | null): LaneStats {\n if (aggregate) {\n const rows = aggregate.byCurrency\n // Pick the single currency with the largest raw total to surface in the header. This\n // is always honest: the operator sees the actual largest cluster of value rather than\n // a synthetic base-currency total that may be partial.\n const headline = rows.length > 0 ? rows[0] : null\n return {\n count: aggregate.count,\n primaryAmount: headline ? headline.total : 0,\n primaryCurrency: headline ? headline.currency : null,\n rows,\n totalInBaseCurrency: aggregate.totalInBaseCurrency,\n baseCurrencyCode: aggregate.baseCurrencyCode,\n convertedAll: aggregate.convertedAll,\n missingRateCurrencies: aggregate.missingRateCurrencies,\n }\n }\n const totalsByCurrency = new Map<string, { total: number; count: number }>()\n for (const deal of deals) {\n if (typeof deal.valueAmount !== 'number' || !Number.isFinite(deal.valueAmount)) continue\n const code = deal.valueCurrency && deal.valueCurrency.length === 3\n ? deal.valueCurrency.toUpperCase()\n : 'USD'\n const entry = totalsByCurrency.get(code) ?? { total: 0, count: 0 }\n entry.total += deal.valueAmount\n entry.count += 1\n totalsByCurrency.set(code, entry)\n }\n const rows: CurrencyRow[] = Array.from(totalsByCurrency.entries())\n .map(([currency, value]) => ({ currency, total: value.total, count: value.count }))\n .sort((a, b) => b.total - a.total)\n if (rows.length === 0) {\n return {\n count: deals.length,\n primaryAmount: 0,\n primaryCurrency: null,\n rows,\n totalInBaseCurrency: 0,\n baseCurrencyCode: null,\n convertedAll: true,\n missingRateCurrencies: [],\n }\n }\n return {\n count: deals.length,\n primaryAmount: rows[0].total,\n primaryCurrency: rows[0].currency,\n rows,\n totalInBaseCurrency: 0,\n baseCurrencyCode: null,\n // We don't have rates client-side; mark as \"not converted\" only when there are\n // multiple currencies so the popover can show a \"configure FX rates\" hint.\n convertedAll: rows.length === 1,\n missingRateCurrencies: rows.length > 1 ? rows.slice(1).map((row) => row.currency) : [],\n }\n}\n\nfunction LaneImpl({\n stage,\n deals,\n aggregate,\n selectedDealIds,\n buildMenuItems,\n activeDragDealId,\n isLoadingMore = false,\n width,\n onToggleSelect,\n onComposeActivity,\n onOpenDetail,\n onQuickAddClick,\n onLoadMore,\n onResize,\n onResizeEnd,\n onResetWidth,\n}: LaneProps): React.ReactElement {\n const t = useT()\n const { setNodeRef, isOver } = useDroppable({\n id: `lane:${stage.id}`,\n data: { type: 'lane', stageId: stage.id },\n })\n const isDragActive = !!activeDragDealId\n const sourceLane = isDragActive && deals.some((deal) => deal.id === activeDragDealId)\n // Highlight drop zone when:\n // - the lane has the pointer over it during a drag, OR\n // - any drag is active and this lane is not the source (so all candidate targets are clearly tagged)\n const showDropHighlight = isOver || (isDragActive && !sourceLane)\n const stats = React.useMemo(() => buildLaneStats(deals, aggregate), [deals, aggregate])\n // Pull boolean out of the Set so each DealCard receives a stable primitive \u2014 that lets\n // React.memo on DealCard skip re-renders when the Set reference changes but its emptiness\n // does not. Flipping false\u2192true (first selection) or true\u2192false (last cleared) forces a\n // full pass over the lane's cards so their checkbox visibility updates atomically.\n const bulkSelectionActive = selectedDealIds.size > 0\n const totalLabel = stats.primaryAmount > 0 && stats.primaryCurrency\n ? `${compactCurrency(stats.primaryAmount)} ${stats.primaryCurrency}`\n : null\n const visibleCount = deals.length\n const totalCount = stats.count\n const hasMoreDeals = totalCount > visibleCount\n const accentClass = getAccentClass(stage.tone)\n const countBadgeClass = getCountBadgeClass(stage.tone)\n\n const handleQuickAdd = React.useCallback(() => {\n onQuickAddClick(stage.id)\n }, [onQuickAddClick, stage.id])\n\n return (\n <div\n className=\"relative flex flex-none flex-col gap-3\"\n style={{ width: typeof width === 'number' && Number.isFinite(width) ? `${width}px` : `${LANE_WIDTH_PX}px` }}\n >\n <div className=\"flex flex-col gap-2.5 overflow-clip rounded-lg bg-muted/40 px-4 py-3.5\">\n <div className={`h-1.5 w-full rounded-sm ${accentClass}`} aria-hidden=\"true\" />\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"truncate text-sm font-bold uppercase leading-normal text-foreground\">\n {stage.label}\n </span>\n <span\n className={`inline-flex shrink-0 items-center rounded-full px-2 py-1 text-xs font-bold leading-normal ${countBadgeClass}`}\n title={\n hasMoreDeals\n ? translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.partialCount',\n 'Showing {visible} of {total}',\n { visible: visibleCount, total: totalCount },\n )\n : undefined\n }\n >\n {totalCount}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {totalLabel ? (\n <span className=\"text-sm font-bold leading-normal text-foreground\">\n {totalLabel}\n </span>\n ) : null}\n {stats.rows.length > 1 ? (\n <LaneCurrencyBreakdown\n rows={stats.rows}\n baseCurrencyCode={stats.baseCurrencyCode}\n totalInBaseCurrency={stats.totalInBaseCurrency}\n convertedAll={stats.convertedAll}\n missingRateCurrencies={stats.missingRateCurrencies}\n triggerLabel={`+${stats.rows.length - 1}`}\n headingLabel={stage.label.toUpperCase()}\n headingCount={totalCount}\n />\n ) : null}\n </div>\n </div>\n </div>\n\n <DashedTileButton\n onClick={handleQuickAdd}\n className=\"h-11\"\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.quickAdd',\n 'Quickly add a deal to {stage}',\n { stage: stage.label },\n )}\n >\n <span className=\"text-base font-bold leading-none\">+</span>\n <span>{translateWithFallback(t, 'customers.deals.kanban.cta.quickDeal', 'Quick deal')}</span>\n </DashedTileButton>\n\n <div\n ref={setNodeRef}\n className={`flex min-h-[40vh] flex-col gap-3 rounded-lg p-1.5 ${\n isOver\n ? 'bg-accent-indigo/10 outline-dashed outline-2 outline-accent-indigo -outline-offset-[2px]'\n : showDropHighlight\n ? 'bg-muted/40 outline-dashed outline-1 outline-muted-foreground/40 -outline-offset-[2px]'\n : ''\n }`}\n >\n {deals.length === 0 ? (\n <EmptyState\n size=\"sm\"\n title={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.empty',\n 'No deals in this stage yet.',\n )}\n />\n ) : (\n deals.map((deal) => (\n <DealCard\n key={deal.id}\n deal={deal}\n selected={selectedDealIds.has(deal.id)}\n bulkSelectionActive={bulkSelectionActive}\n buildMenuItems={buildMenuItems}\n isActiveDrag={activeDragDealId === deal.id}\n onToggleSelect={onToggleSelect}\n onComposeActivity={onComposeActivity}\n onOpenDetail={onOpenDetail}\n />\n ))\n )}\n {hasMoreDeals && onLoadMore ? (\n <DashedTileButton\n onClick={() => onLoadMore(stage.id)}\n disabled={isLoadingMore}\n className=\"mt-0.75 px-3 py-2.5 text-muted-foreground\"\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.loadMore',\n 'Load more deals in {stage}',\n { stage: stage.label },\n )}\n >\n {isLoadingMore\n ? translateWithFallback(t, 'customers.deals.kanban.lane.loadingMore', 'Loading\u2026')\n : translateWithFallback(\n t,\n 'customers.deals.kanban.lane.loadMore',\n 'Show more ({remaining})',\n { remaining: totalCount - visibleCount },\n )}\n </DashedTileButton>\n ) : null}\n </div>\n {onResize ? (\n <LaneResizeHandle\n onResize={(delta) => onResize(stage.id, delta)}\n onResizeEnd={() => onResizeEnd?.(stage.id)}\n onReset={() => onResetWidth?.(stage.id)}\n />\n ) : null}\n </div>\n )\n}\n\n// Memoize the lane so changes in a different lane (or in the active drag) don't re-render us.\n// Page passes booleans (isDragActive) instead of activeDragDealId so most lanes can skip re-rendering.\nexport const Lane = React.memo(LaneImpl, (prev, next) => {\n if (prev.stage !== next.stage) return false\n if (prev.deals !== next.deals) return false\n if (prev.aggregate !== next.aggregate) return false\n if (prev.selectedDealIds !== next.selectedDealIds) return false\n if (prev.buildMenuItems !== next.buildMenuItems) return false\n if (prev.activeDragDealId !== next.activeDragDealId) return false\n if (prev.isLoadingMore !== next.isLoadingMore) return false\n if (prev.width !== next.width) return false\n return true\n})\n\nexport default Lane\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useDroppable } from '@dnd-kit/core'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport type { RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport type { FilterOptionTone } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { toneToDotClass } from './toneClasses'\nimport { DealCard, type DealCardData } from './DealCard'\nimport { DashedTileButton } from './DashedTileButton'\nimport { LANE_WIDTH_PX } from './constants'\nimport { LaneCurrencyBreakdown } from './LaneCurrencyBreakdown'\nimport { LaneResizeHandle } from './LaneResizeHandle'\n\nexport type LaneStage = {\n id: string\n label: string\n tone: FilterOptionTone | null\n}\n\nexport type LaneAggregateProp = {\n count: number\n totalInBaseCurrency: number\n byCurrency: Array<{ currency: string; total: number; count: number }>\n baseCurrencyCode: string | null\n /** `true` when every currency in `byCurrency` was convertible to the base currency. */\n convertedAll: boolean\n /** Currencies present in `byCurrency` that lack an FX rate to base \u2014 excluded from totals. */\n missingRateCurrencies: string[]\n}\n\ntype LaneProps = {\n stage: LaneStage\n deals: DealCardData[]\n /** Server-side aggregate for accurate counts/totals across all deals in this stage, not just loaded ones */\n aggregate?: LaneAggregateProp | null\n paginationTotal?: number\n selectedDealIds: Set<string>\n buildMenuItems: (deal: DealCardData) => RowActionItem[]\n activeDragDealId?: string | null\n isLoadingMore?: boolean\n /** Pixel width override (when the user has resized the column). Falls back to the default class. */\n width?: number | null\n onToggleSelect: (dealId: string) => void\n onComposeActivity: (dealId: string, type: 'call' | 'email' | 'note') => void\n onOpenDetail: (dealId: string) => void\n onQuickAddClick: (stageId: string) => void\n onLoadMore?: (stageId: string) => void\n /** Drag-resize callbacks. When provided, a vertical drag handle is rendered on the lane edge. */\n onResize?: (stageId: string, deltaPx: number) => void\n onResizeEnd?: (stageId: string) => void\n onResetWidth?: (stageId: string) => void\n}\n\n// 4px color bar tone \u2014 saturated icon tokens shared with the status filter pills\n// (toneClasses.ts) so the lane bar and the filter dots stay in lockstep (#5107 review).\nfunction getAccentClass(tone: FilterOptionTone | null): string {\n return toneToDotClass(tone, 'bg-border')\n}\n\n// Count pill bg uses very-light status background, text uses status text color\nconst COUNT_BADGE_TONE_CLASS: Record<FilterOptionTone, string> = {\n success: 'bg-status-success-bg text-status-success-text',\n error: 'bg-status-error-bg text-status-error-text',\n warning: 'bg-status-warning-bg text-status-warning-text',\n info: 'bg-status-info-bg text-status-info-text',\n neutral: 'bg-status-neutral-bg text-status-neutral-text',\n brand: 'bg-brand-violet/14 text-brand-violet',\n pink: 'bg-status-pink-bg text-status-pink-text',\n}\n\nfunction getCountBadgeClass(tone: FilterOptionTone | null): string {\n if (tone && tone in COUNT_BADGE_TONE_CLASS) return COUNT_BADGE_TONE_CLASS[tone]\n return 'bg-muted text-muted-foreground'\n}\n\nfunction compactCurrency(amount: number): string {\n if (amount >= 1_000_000) {\n const value = amount / 1_000_000\n return value >= 100 ? `${Math.round(value)}M` : `${value.toFixed(1).replace(/\\.0$/, '')}M`\n }\n if (amount >= 1_000) {\n const value = amount / 1_000\n return value >= 100 ? `${Math.round(value)}k` : `${value.toFixed(1).replace(/\\.0$/, '')}k`\n }\n return String(Math.round(amount))\n}\n\ntype CurrencyRow = { currency: string; total: number; count: number }\n\ntype LaneStats = {\n count: number\n /** Headline amount and the currency it is denominated in (largest single currency, raw, never converted). */\n primaryAmount: number\n primaryCurrency: string | null\n /** Per-currency rows sorted desc by total \u2014 the popover renders these one per line. */\n rows: CurrencyRow[]\n /** Sum of `rows` converted to base currency (zero when no base is configured / nothing converted). */\n totalInBaseCurrency: number\n baseCurrencyCode: string | null\n convertedAll: boolean\n missingRateCurrencies: string[]\n}\n\n/**\n * Build display stats.\n * - If a server aggregate is provided, prefer it (covers all deals, converts to tenant base currency).\n * - Otherwise fall back to summing loaded deals (used before aggregate query loads).\n *\n * The headline currency on the lane header is intentionally the LARGEST raw currency\n * (not the base-currency converted total) because the converted total can be misleading\n * when FX rates are missing \u2014 operators see a smaller number than the headline suggests.\n * The base-currency converted total is reserved for the breakdown popover footer, where\n * a partial-conversion indicator can disclose what's excluded.\n */\nfunction buildLaneStats(deals: DealCardData[], aggregate?: LaneAggregateProp | null): LaneStats {\n if (aggregate) {\n const rows = aggregate.byCurrency\n // Pick the single currency with the largest raw total to surface in the header. This\n // is always honest: the operator sees the actual largest cluster of value rather than\n // a synthetic base-currency total that may be partial.\n const headline = rows.length > 0 ? rows[0] : null\n return {\n count: aggregate.count,\n primaryAmount: headline ? headline.total : 0,\n primaryCurrency: headline ? headline.currency : null,\n rows,\n totalInBaseCurrency: aggregate.totalInBaseCurrency,\n baseCurrencyCode: aggregate.baseCurrencyCode,\n convertedAll: aggregate.convertedAll,\n missingRateCurrencies: aggregate.missingRateCurrencies,\n }\n }\n const totalsByCurrency = new Map<string, { total: number; count: number }>()\n for (const deal of deals) {\n if (typeof deal.valueAmount !== 'number' || !Number.isFinite(deal.valueAmount)) continue\n const code = deal.valueCurrency && deal.valueCurrency.length === 3\n ? deal.valueCurrency.toUpperCase()\n : 'USD'\n const entry = totalsByCurrency.get(code) ?? { total: 0, count: 0 }\n entry.total += deal.valueAmount\n entry.count += 1\n totalsByCurrency.set(code, entry)\n }\n const rows: CurrencyRow[] = Array.from(totalsByCurrency.entries())\n .map(([currency, value]) => ({ currency, total: value.total, count: value.count }))\n .sort((a, b) => b.total - a.total)\n if (rows.length === 0) {\n return {\n count: deals.length,\n primaryAmount: 0,\n primaryCurrency: null,\n rows,\n totalInBaseCurrency: 0,\n baseCurrencyCode: null,\n convertedAll: true,\n missingRateCurrencies: [],\n }\n }\n return {\n count: deals.length,\n primaryAmount: rows[0].total,\n primaryCurrency: rows[0].currency,\n rows,\n totalInBaseCurrency: 0,\n baseCurrencyCode: null,\n // We don't have rates client-side; mark as \"not converted\" only when there are\n // multiple currencies so the popover can show a \"configure FX rates\" hint.\n convertedAll: rows.length === 1,\n missingRateCurrencies: rows.length > 1 ? rows.slice(1).map((row) => row.currency) : [],\n }\n}\n\nfunction LaneImpl({\n stage,\n deals,\n aggregate,\n paginationTotal,\n selectedDealIds,\n buildMenuItems,\n activeDragDealId,\n isLoadingMore = false,\n width,\n onToggleSelect,\n onComposeActivity,\n onOpenDetail,\n onQuickAddClick,\n onLoadMore,\n onResize,\n onResizeEnd,\n onResetWidth,\n}: LaneProps): React.ReactElement {\n const t = useT()\n const { setNodeRef, isOver } = useDroppable({\n id: `lane:${stage.id}`,\n data: { type: 'lane', stageId: stage.id },\n })\n const isDragActive = !!activeDragDealId\n const sourceLane = isDragActive && deals.some((deal) => deal.id === activeDragDealId)\n // Highlight drop zone when:\n // - the lane has the pointer over it during a drag, OR\n // - any drag is active and this lane is not the source (so all candidate targets are clearly tagged)\n const showDropHighlight = isOver || (isDragActive && !sourceLane)\n const stats = React.useMemo(() => buildLaneStats(deals, aggregate), [deals, aggregate])\n // Pull boolean out of the Set so each DealCard receives a stable primitive \u2014 that lets\n // React.memo on DealCard skip re-renders when the Set reference changes but its emptiness\n // does not. Flipping false\u2192true (first selection) or true\u2192false (last cleared) forces a\n // full pass over the lane's cards so their checkbox visibility updates atomically.\n const bulkSelectionActive = selectedDealIds.size > 0\n const totalLabel = stats.primaryAmount > 0 && stats.primaryCurrency\n ? `${compactCurrency(stats.primaryAmount)} ${stats.primaryCurrency}`\n : null\n const visibleCount = deals.length\n const totalCount = stats.count\n const remainingCount = (paginationTotal ?? totalCount) - visibleCount\n const hasMoreDeals = remainingCount > 0\n const accentClass = getAccentClass(stage.tone)\n const countBadgeClass = getCountBadgeClass(stage.tone)\n\n const handleQuickAdd = React.useCallback(() => {\n onQuickAddClick(stage.id)\n }, [onQuickAddClick, stage.id])\n\n return (\n <div\n className=\"relative flex flex-none flex-col gap-3\"\n style={{ width: typeof width === 'number' && Number.isFinite(width) ? `${width}px` : `${LANE_WIDTH_PX}px` }}\n >\n <div className=\"flex flex-col gap-2.5 overflow-clip rounded-lg bg-muted/40 px-4 py-3.5\">\n <div className={`h-1.5 w-full rounded-sm ${accentClass}`} aria-hidden=\"true\" />\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"truncate text-sm font-bold uppercase leading-normal text-foreground\">\n {stage.label}\n </span>\n <span\n className={`inline-flex shrink-0 items-center rounded-full px-2 py-1 text-xs font-bold leading-normal ${countBadgeClass}`}\n title={\n hasMoreDeals\n ? translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.partialCount',\n 'Showing {visible} of {total}',\n { visible: visibleCount, total: totalCount },\n )\n : undefined\n }\n >\n {totalCount}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {totalLabel ? (\n <span className=\"text-sm font-bold leading-normal text-foreground\">\n {totalLabel}\n </span>\n ) : null}\n {stats.rows.length > 1 ? (\n <LaneCurrencyBreakdown\n rows={stats.rows}\n baseCurrencyCode={stats.baseCurrencyCode}\n totalInBaseCurrency={stats.totalInBaseCurrency}\n convertedAll={stats.convertedAll}\n missingRateCurrencies={stats.missingRateCurrencies}\n triggerLabel={`+${stats.rows.length - 1}`}\n headingLabel={stage.label.toUpperCase()}\n headingCount={totalCount}\n />\n ) : null}\n </div>\n </div>\n </div>\n\n <DashedTileButton\n onClick={handleQuickAdd}\n className=\"h-11\"\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.quickAdd',\n 'Quickly add a deal to {stage}',\n { stage: stage.label },\n )}\n >\n <span className=\"text-base font-bold leading-none\">+</span>\n <span>{translateWithFallback(t, 'customers.deals.kanban.cta.quickDeal', 'Quick deal')}</span>\n </DashedTileButton>\n\n <div\n ref={setNodeRef}\n className={`flex min-h-[40vh] flex-col gap-3 rounded-lg p-1.5 ${\n isOver\n ? 'bg-accent-indigo/10 outline-dashed outline-2 outline-accent-indigo -outline-offset-[2px]'\n : showDropHighlight\n ? 'bg-muted/40 outline-dashed outline-1 outline-muted-foreground/40 -outline-offset-[2px]'\n : ''\n }`}\n >\n {deals.length === 0 ? (\n <EmptyState\n size=\"sm\"\n title={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.empty',\n 'No deals in this stage yet.',\n )}\n />\n ) : (\n deals.map((deal) => (\n <DealCard\n key={deal.id}\n deal={deal}\n selected={selectedDealIds.has(deal.id)}\n bulkSelectionActive={bulkSelectionActive}\n buildMenuItems={buildMenuItems}\n isActiveDrag={activeDragDealId === deal.id}\n onToggleSelect={onToggleSelect}\n onComposeActivity={onComposeActivity}\n onOpenDetail={onOpenDetail}\n />\n ))\n )}\n {hasMoreDeals && onLoadMore ? (\n <DashedTileButton\n onClick={() => onLoadMore(stage.id)}\n disabled={isLoadingMore}\n className=\"mt-0.75 px-3 py-2.5 text-muted-foreground\"\n ariaLabel={translateWithFallback(\n t,\n 'customers.deals.kanban.lane.aria.loadMore',\n 'Load more deals in {stage}',\n { stage: stage.label },\n )}\n >\n {isLoadingMore\n ? translateWithFallback(t, 'customers.deals.kanban.lane.loadingMore', 'Loading\u2026')\n : translateWithFallback(\n t,\n 'customers.deals.kanban.lane.loadMore',\n 'Show more ({remaining})',\n { remaining: remainingCount },\n )}\n </DashedTileButton>\n ) : null}\n </div>\n {onResize ? (\n <LaneResizeHandle\n onResize={(delta) => onResize(stage.id, delta)}\n onResizeEnd={() => onResizeEnd?.(stage.id)}\n onReset={() => onResetWidth?.(stage.id)}\n />\n ) : null}\n </div>\n )\n}\n\n// Memoize the lane so changes in a different lane (or in the active drag) don't re-render us.\n// Page passes booleans (isDragActive) instead of activeDragDealId so most lanes can skip re-rendering.\nexport const Lane = React.memo(LaneImpl, (prev, next) => {\n if (prev.stage !== next.stage) return false\n if (prev.deals !== next.deals) return false\n if (prev.aggregate !== next.aggregate) return false\n if (prev.paginationTotal !== next.paginationTotal) return false\n if (prev.selectedDealIds !== next.selectedDealIds) return false\n if (prev.buildMenuItems !== next.buildMenuItems) return false\n if (prev.activeDragDealId !== next.activeDragDealId) return false\n if (prev.isLoadingMore !== next.isLoadingMore) return false\n if (prev.width !== next.width) return false\n return true\n})\n\nexport default Lane\n"],
5
+ "mappings": "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",
6
6
  "names": ["rows"]
7
7
  }
@@ -443,10 +443,6 @@ function DealsKanbanPage() {
443
443
  });
444
444
  const [extraCardsByStage, setExtraCardsByStage] = React.useState({});
445
445
  const [loadingMoreByStage, setLoadingMoreByStage] = React.useState({});
446
- React.useEffect(() => {
447
- setExtraCardsByStage({});
448
- setLoadingMoreByStage({});
449
- }, [selectedPipelineId, search, statusFilters, ownerFilters, peopleFilters, companyFilters, closeDateFilter, currencyFilter]);
450
446
  const filterSignature = React.useMemo(
451
447
  () => ({
452
448
  pipelineId: selectedPipelineId,
@@ -526,28 +522,38 @@ function DealsKanbanPage() {
526
522
  const mapped = mapSortOptionToApi(sortBy);
527
523
  return mapped ?? { sortField: "updatedAt", sortDir: "desc" };
528
524
  }, [sortBy]);
525
+ const laneQueryKey = React.useMemo(
526
+ () => [
527
+ "customers",
528
+ "deals",
529
+ "kanban-lane",
530
+ `scope:${scopeVersion}`,
531
+ `organization:${activeOrgId ?? "none"}`,
532
+ `pipeline:${filterSignature.pipelineId ?? "none"}`,
533
+ `search:${filterSignature.search}`,
534
+ `status:${filterSignature.status}`,
535
+ `owners:${filterSignature.owners}`,
536
+ `people:${filterSignature.people}`,
537
+ `companies:${filterSignature.companies}`,
538
+ `close:${filterSignature.closeFrom}-${filterSignature.closeTo}`,
539
+ `sort:${apiSort.sortField}:${apiSort.sortDir}`,
540
+ `currency:${filterSignature.currency}`
541
+ ],
542
+ [scopeVersion, activeOrgId, filterSignature, apiSort]
543
+ );
544
+ const paginationGeneration = React.useRef(0);
545
+ React.useEffect(() => {
546
+ paginationGeneration.current += 1;
547
+ setExtraCardsByStage({});
548
+ setLoadingMoreByStage({});
549
+ return () => {
550
+ paginationGeneration.current += 1;
551
+ };
552
+ }, [laneQueryKey]);
529
553
  const laneQueries = useQueries({
530
554
  queries: lanes.map((stage) => {
531
555
  return {
532
- queryKey: [
533
- "customers",
534
- "deals",
535
- "kanban-lane",
536
- `scope:${scopeVersion}`,
537
- `pipeline:${filterSignature.pipelineId ?? "none"}`,
538
- `search:${filterSignature.search}`,
539
- `status:${filterSignature.status}`,
540
- `owners:${filterSignature.owners}`,
541
- `people:${filterSignature.people}`,
542
- `companies:${filterSignature.companies}`,
543
- `close:${filterSignature.closeFrom}-${filterSignature.closeTo}`,
544
- // Sort affects which 25 deals come back on page 1, so it MUST be in the key —
545
- // otherwise switching from "Value (high to low)" to "Close (soonest)" reuses
546
- // the old (wrong-sort) cache and silently keeps the wrong slice on screen.
547
- `sort:${apiSort.sortField}:${apiSort.sortDir}`,
548
- `currency:${filterSignature.currency}`,
549
- `stage:${stage.id}`
550
- ],
556
+ queryKey: [...laneQueryKey, `stage:${stage.id}`],
551
557
  enabled: !!selectedPipelineId,
552
558
  staleTime: 3e4,
553
559
  queryFn: async () => {
@@ -688,6 +694,7 @@ function DealsKanbanPage() {
688
694
  if (loadingMoreByStage[stageId]) return;
689
695
  const already = extraCardsByStage[stageId]?.length ?? 0;
690
696
  const nextPage = Math.floor((LANE_PAGE_SIZE + already) / LANE_PAGE_SIZE) + 1;
697
+ const requestGeneration = paginationGeneration.current;
691
698
  setLoadingMoreByStage((prev) => ({ ...prev, [stageId]: true }));
692
699
  try {
693
700
  const params = new URLSearchParams();
@@ -708,18 +715,20 @@ function DealsKanbanPage() {
708
715
  const call = await apiCall(
709
716
  `/api/customers/deals?${params.toString()}`
710
717
  );
711
- if (!call.ok) return;
718
+ if (requestGeneration !== paginationGeneration.current || !call.ok) return;
712
719
  const items = Array.isArray(call.result?.items) ? call.result.items : [];
713
720
  setExtraCardsByStage((prev) => ({
714
721
  ...prev,
715
722
  [stageId]: [...prev[stageId] ?? [], ...items]
716
723
  }));
717
724
  } finally {
718
- setLoadingMoreByStage((prev) => {
719
- const next = { ...prev };
720
- delete next[stageId];
721
- return next;
722
- });
725
+ if (requestGeneration === paginationGeneration.current) {
726
+ setLoadingMoreByStage((prev) => {
727
+ const next = { ...prev };
728
+ delete next[stageId];
729
+ return next;
730
+ });
731
+ }
723
732
  }
724
733
  },
725
734
  [
@@ -733,7 +742,9 @@ function DealsKanbanPage() {
733
742
  companyFilters,
734
743
  closeDateFilter.from,
735
744
  closeDateFilter.to,
736
- apiSort
745
+ currencyFilter,
746
+ apiSort,
747
+ laneQueryKey
737
748
  ]
738
749
  );
739
750
  const isInitialLoading = !!selectedPipelineId && lanes.length === 0 ? stagesQuery.isLoading : laneQueries.some((laneQuery) => laneQuery.isLoading && !laneQuery.data);
@@ -2301,7 +2312,7 @@ function DealsKanbanPage() {
2301
2312
  className: "h-[50vh] w-full"
2302
2313
  }
2303
2314
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
2304
- lanes.map((stage) => {
2315
+ lanes.map((stage, stageIndex) => {
2305
2316
  const laneDeals = sortedDealsByStage.get(stage.id) ?? EMPTY_DEAL_ARRAY;
2306
2317
  return /* @__PURE__ */ jsx(
2307
2318
  Lane,
@@ -2309,6 +2320,7 @@ function DealsKanbanPage() {
2309
2320
  stage,
2310
2321
  deals: laneDeals,
2311
2322
  aggregate: aggregateByStage.get(stage.id) ?? null,
2323
+ paginationTotal: laneQueries[stageIndex]?.data?.total ?? 0,
2312
2324
  selectedDealIds,
2313
2325
  buildMenuItems,
2314
2326
  activeDragDealId,