@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
@@ -28,16 +28,16 @@ export const ChipButton = React.forwardRef<HTMLButtonElement, ChipButtonProps>(
28
28
  onClick={onClick}
29
29
  aria-label={ariaLabel ?? (label ? `${label}: ${value}` : value)}
30
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 ${
31
+ className={`inline-flex min-w-0 max-w-full 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
32
  active ? 'border-foreground/40 bg-muted text-foreground' : 'text-foreground'
33
33
  }`}
34
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'}>
35
+ {label ? <span className="shrink-0 font-normal text-muted-foreground">{label}:</span> : null}
36
+ <span className={`min-w-0 truncate ${label ? 'font-semibold text-foreground' : 'font-normal text-foreground'}`}>
37
37
  {value}
38
38
  </span>
39
39
  {withChevron ? (
40
- <ChevronDown className="size-3 text-muted-foreground" aria-hidden="true" />
40
+ <ChevronDown className="size-3 shrink-0 text-muted-foreground" aria-hidden="true" />
41
41
  ) : null}
42
42
  </Button>
43
43
  )
@@ -36,6 +36,7 @@ type LaneProps = {
36
36
  deals: DealCardData[]
37
37
  /** Server-side aggregate for accurate counts/totals across all deals in this stage, not just loaded ones */
38
38
  aggregate?: LaneAggregateProp | null
39
+ paginationTotal?: number
39
40
  selectedDealIds: Set<string>
40
41
  buildMenuItems: (deal: DealCardData) => RowActionItem[]
41
42
  activeDragDealId?: string | null
@@ -176,6 +177,7 @@ function LaneImpl({
176
177
  stage,
177
178
  deals,
178
179
  aggregate,
180
+ paginationTotal,
179
181
  selectedDealIds,
180
182
  buildMenuItems,
181
183
  activeDragDealId,
@@ -212,7 +214,8 @@ function LaneImpl({
212
214
  : null
213
215
  const visibleCount = deals.length
214
216
  const totalCount = stats.count
215
- const hasMoreDeals = totalCount > visibleCount
217
+ const remainingCount = (paginationTotal ?? totalCount) - visibleCount
218
+ const hasMoreDeals = remainingCount > 0
216
219
  const accentClass = getAccentClass(stage.tone)
217
220
  const countBadgeClass = getCountBadgeClass(stage.tone)
218
221
 
@@ -336,7 +339,7 @@ function LaneImpl({
336
339
  t,
337
340
  'customers.deals.kanban.lane.loadMore',
338
341
  'Show more ({remaining})',
339
- { remaining: totalCount - visibleCount },
342
+ { remaining: remainingCount },
340
343
  )}
341
344
  </DashedTileButton>
342
345
  ) : null}
@@ -358,6 +361,7 @@ export const Lane = React.memo(LaneImpl, (prev, next) => {
358
361
  if (prev.stage !== next.stage) return false
359
362
  if (prev.deals !== next.deals) return false
360
363
  if (prev.aggregate !== next.aggregate) return false
364
+ if (prev.paginationTotal !== next.paginationTotal) return false
361
365
  if (prev.selectedDealIds !== next.selectedDealIds) return false
362
366
  if (prev.buildMenuItems !== next.buildMenuItems) return false
363
367
  if (prev.activeDragDealId !== next.activeDragDealId) return false
@@ -613,11 +613,6 @@ export default function DealsKanbanPage(): React.ReactElement {
613
613
  // fetched on demand and appended here, so the cached page-1 cards stay in place (no full lane refetch).
614
614
  const [extraCardsByStage, setExtraCardsByStage] = React.useState<Record<string, DealApiRecord[]>>({})
615
615
  const [loadingMoreByStage, setLoadingMoreByStage] = React.useState<Record<string, boolean>>({})
616
- // Reset extra-cards + loading state when any filter changes
617
- React.useEffect(() => {
618
- setExtraCardsByStage({})
619
- setLoadingMoreByStage({})
620
- }, [selectedPipelineId, search, statusFilters, ownerFilters, peopleFilters, companyFilters, closeDateFilter, currencyFilter])
621
616
 
622
617
  // Filter signature shared by every per-lane query so all lanes invalidate together when filters change
623
618
  const filterSignature = React.useMemo(
@@ -636,7 +631,7 @@ export default function DealsKanbanPage(): React.ReactElement {
636
631
  )
637
632
 
638
633
  // Aggregate query — gives accurate per-stage counts + totals (in base currency) across the entire pipeline,
639
- // not just the loaded slice. Drives both lane headers and pagination decisions.
634
+ // not just the loaded slice. Drives lane headers and currency breakdowns.
640
635
  const aggregateQuery = useQuery<StageAggregateResponse | null>({
641
636
  queryKey: [
642
637
  'customers',
@@ -711,28 +706,40 @@ export default function DealsKanbanPage(): React.ReactElement {
711
706
  return mapped ?? { sortField: 'updatedAt' as const, sortDir: 'desc' as const }
712
707
  }, [sortBy])
713
708
 
709
+ const laneQueryKey = React.useMemo(
710
+ () => [
711
+ 'customers',
712
+ 'deals',
713
+ 'kanban-lane',
714
+ `scope:${scopeVersion}`,
715
+ `organization:${activeOrgId ?? 'none'}`,
716
+ `pipeline:${filterSignature.pipelineId ?? 'none'}`,
717
+ `search:${filterSignature.search}`,
718
+ `status:${filterSignature.status}`,
719
+ `owners:${filterSignature.owners}`,
720
+ `people:${filterSignature.people}`,
721
+ `companies:${filterSignature.companies}`,
722
+ `close:${filterSignature.closeFrom}-${filterSignature.closeTo}`,
723
+ `sort:${apiSort.sortField}:${apiSort.sortDir}`,
724
+ `currency:${filterSignature.currency}`,
725
+ ],
726
+ [scopeVersion, activeOrgId, filterSignature, apiSort],
727
+ )
728
+ const paginationGeneration = React.useRef(0)
729
+
730
+ React.useEffect(() => {
731
+ paginationGeneration.current += 1
732
+ setExtraCardsByStage({})
733
+ setLoadingMoreByStage({})
734
+ return () => {
735
+ paginationGeneration.current += 1
736
+ }
737
+ }, [laneQueryKey])
738
+
714
739
  const laneQueries = useQueries({
715
740
  queries: lanes.map((stage) => {
716
741
  return {
717
- queryKey: [
718
- 'customers',
719
- 'deals',
720
- 'kanban-lane',
721
- `scope:${scopeVersion}`,
722
- `pipeline:${filterSignature.pipelineId ?? 'none'}`,
723
- `search:${filterSignature.search}`,
724
- `status:${filterSignature.status}`,
725
- `owners:${filterSignature.owners}`,
726
- `people:${filterSignature.people}`,
727
- `companies:${filterSignature.companies}`,
728
- `close:${filterSignature.closeFrom}-${filterSignature.closeTo}`,
729
- // Sort affects which 25 deals come back on page 1, so it MUST be in the key —
730
- // otherwise switching from "Value (high to low)" to "Close (soonest)" reuses
731
- // the old (wrong-sort) cache and silently keeps the wrong slice on screen.
732
- `sort:${apiSort.sortField}:${apiSort.sortDir}`,
733
- `currency:${filterSignature.currency}`,
734
- `stage:${stage.id}`,
735
- ],
742
+ queryKey: [...laneQueryKey, `stage:${stage.id}`],
736
743
  enabled: !!selectedPipelineId,
737
744
  staleTime: 30_000,
738
745
  queryFn: async () => {
@@ -900,6 +907,7 @@ export default function DealsKanbanPage(): React.ReactElement {
900
907
  // Page 1 = first 25 (initial useQueries fetch). Subsequent pages come from extraCardsByStage.
901
908
  const already = extraCardsByStage[stageId]?.length ?? 0
902
909
  const nextPage = Math.floor((LANE_PAGE_SIZE + already) / LANE_PAGE_SIZE) + 1
910
+ const requestGeneration = paginationGeneration.current
903
911
  setLoadingMoreByStage((prev) => ({ ...prev, [stageId]: true }))
904
912
  try {
905
913
  const params = new URLSearchParams()
@@ -922,18 +930,20 @@ export default function DealsKanbanPage(): React.ReactElement {
922
930
  const call = await apiCall<{ items?: DealApiRecord[] }>(
923
931
  `/api/customers/deals?${params.toString()}`,
924
932
  )
925
- if (!call.ok) return
933
+ if (requestGeneration !== paginationGeneration.current || !call.ok) return
926
934
  const items = Array.isArray(call.result?.items) ? (call.result!.items as DealApiRecord[]) : []
927
935
  setExtraCardsByStage((prev) => ({
928
936
  ...prev,
929
937
  [stageId]: [...(prev[stageId] ?? []), ...items],
930
938
  }))
931
939
  } finally {
932
- setLoadingMoreByStage((prev) => {
933
- const next = { ...prev }
934
- delete next[stageId]
935
- return next
936
- })
940
+ if (requestGeneration === paginationGeneration.current) {
941
+ setLoadingMoreByStage((prev) => {
942
+ const next = { ...prev }
943
+ delete next[stageId]
944
+ return next
945
+ })
946
+ }
937
947
  }
938
948
  },
939
949
  [
@@ -947,7 +957,9 @@ export default function DealsKanbanPage(): React.ReactElement {
947
957
  companyFilters,
948
958
  closeDateFilter.from,
949
959
  closeDateFilter.to,
960
+ currencyFilter,
950
961
  apiSort,
962
+ laneQueryKey,
951
963
  ],
952
964
  )
953
965
 
@@ -2841,7 +2853,7 @@ export default function DealsKanbanPage(): React.ReactElement {
2841
2853
  />
2842
2854
  ) : (
2843
2855
  <>
2844
- {lanes.map((stage) => {
2856
+ {lanes.map((stage, stageIndex) => {
2845
2857
  const laneDeals = sortedDealsByStage.get(stage.id) ?? EMPTY_DEAL_ARRAY
2846
2858
  return (
2847
2859
  <Lane
@@ -2849,6 +2861,7 @@ export default function DealsKanbanPage(): React.ReactElement {
2849
2861
  stage={stage}
2850
2862
  deals={laneDeals}
2851
2863
  aggregate={aggregateByStage.get(stage.id) ?? null}
2864
+ paginationTotal={laneQueries[stageIndex]?.data?.total ?? 0}
2852
2865
  selectedDealIds={selectedDealIds}
2853
2866
  buildMenuItems={buildMenuItems}
2854
2867
  activeDragDealId={activeDragDealId}
@@ -8,7 +8,8 @@ import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-
8
8
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
11
- import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
12
+ import { formatDisplayDateTime } from '@open-mercato/ui/primitives/date-format'
12
13
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
13
14
 
14
15
  type PushDeliveryStatus = 'pending' | 'sending' | 'sent' | 'failed' | 'skipped' | 'expired'
@@ -44,15 +45,8 @@ const statusVariant: StatusMap<PushDeliveryStatus> = {
44
45
  expired: 'warning',
45
46
  }
46
47
 
47
- function formatDate(value: string | null, t: (key: string) => string) {
48
- if (!value) return t('push_notifications.deliveries.noValue')
49
- try {
50
- const date = new Date(value)
51
- if (Number.isNaN(date.getTime())) return t('push_notifications.deliveries.noValue')
52
- return date.toLocaleString()
53
- } catch {
54
- return t('push_notifications.deliveries.noValue')
55
- }
48
+ function formatDate(value: string | null, locale: string | undefined, t: (key: string) => string) {
49
+ return formatDisplayDateTime(value, locale) ?? t('push_notifications.deliveries.noValue')
56
50
  }
57
51
 
58
52
  export default function PushDeliveriesListPage() {
@@ -64,6 +58,7 @@ export default function PushDeliveriesListPage() {
64
58
  const [isLoading, setIsLoading] = React.useState(true)
65
59
  const scopeVersion = useOrganizationScopeVersion()
66
60
  const t = useT()
61
+ const dateLocale = useLocale()
67
62
  const [filterValues, setFilterValues] = React.useState<FilterValues>({})
68
63
  const [userOptions, setUserOptions] = React.useState<{ value: string; label: string; description?: string | null }[]>([])
69
64
 
@@ -210,14 +205,14 @@ export default function PushDeliveriesListPage() {
210
205
  {
211
206
  accessorKey: 'created_at',
212
207
  header: t('push_notifications.deliveries.columns.created'),
213
- cell: ({ row }) => formatDate(row.original.created_at, t),
208
+ cell: ({ row }) => formatDate(row.original.created_at, dateLocale, t),
214
209
  },
215
210
  {
216
211
  accessorKey: 'sent_at',
217
212
  header: t('push_notifications.deliveries.columns.sent'),
218
- cell: ({ row }) => formatDate(row.original.sent_at, t),
213
+ cell: ({ row }) => formatDate(row.original.sent_at, dateLocale, t),
219
214
  },
220
- ], [t, userLabelById])
215
+ ], [t, dateLocale, userLabelById])
221
216
 
222
217
  return (
223
218
  <Page>