@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
@@ -8,7 +8,8 @@ import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
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
  const statusVariant = {
13
14
  pending: "info",
14
15
  sending: "info",
@@ -17,15 +18,8 @@ const statusVariant = {
17
18
  skipped: "neutral",
18
19
  expired: "warning"
19
20
  };
20
- function formatDate(value, t) {
21
- if (!value) return t("push_notifications.deliveries.noValue");
22
- try {
23
- const date = new Date(value);
24
- if (Number.isNaN(date.getTime())) return t("push_notifications.deliveries.noValue");
25
- return date.toLocaleString();
26
- } catch {
27
- return t("push_notifications.deliveries.noValue");
28
- }
21
+ function formatDate(value, locale, t) {
22
+ return formatDisplayDateTime(value, locale) ?? t("push_notifications.deliveries.noValue");
29
23
  }
30
24
  function PushDeliveriesListPage() {
31
25
  const [rows, setRows] = React.useState([]);
@@ -36,6 +30,7 @@ function PushDeliveriesListPage() {
36
30
  const [isLoading, setIsLoading] = React.useState(true);
37
31
  const scopeVersion = useOrganizationScopeVersion();
38
32
  const t = useT();
33
+ const dateLocale = useLocale();
39
34
  const [filterValues, setFilterValues] = React.useState({});
40
35
  const [userOptions, setUserOptions] = React.useState([]);
41
36
  const loadUserOptions = React.useCallback(async (query) => {
@@ -175,14 +170,14 @@ function PushDeliveriesListPage() {
175
170
  {
176
171
  accessorKey: "created_at",
177
172
  header: t("push_notifications.deliveries.columns.created"),
178
- cell: ({ row }) => formatDate(row.original.created_at, t)
173
+ cell: ({ row }) => formatDate(row.original.created_at, dateLocale, t)
179
174
  },
180
175
  {
181
176
  accessorKey: "sent_at",
182
177
  header: t("push_notifications.deliveries.columns.sent"),
183
- cell: ({ row }) => formatDate(row.original.sent_at, t)
178
+ cell: ({ row }) => formatDate(row.original.sent_at, dateLocale, t)
184
179
  }
185
- ], [t, userLabelById]);
180
+ ], [t, dateLocale, userLabelById]);
186
181
  return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
187
182
  DataTable,
188
183
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/push_notifications/backend/push_notifications/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype PushDeliveryStatus = 'pending' | 'sending' | 'sent' | 'failed' | 'skipped' | 'expired'\n\ntype Row = {\n id: string\n notification_type_id: string\n user_id: string\n provider: string\n token_snapshot: string\n status: PushDeliveryStatus\n attempts: number\n last_error: string | null\n created_at: string | null\n sent_at: string | null\n}\n\ntype ResponsePayload = {\n items: Row[]\n total: number\n page?: number\n pageSize?: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\nconst statusVariant: StatusMap<PushDeliveryStatus> = {\n pending: 'info',\n sending: 'info',\n sent: 'success',\n failed: 'error',\n skipped: 'neutral',\n expired: 'warning',\n}\n\nfunction formatDate(value: string | null, t: (key: string) => string) {\n if (!value) return t('push_notifications.deliveries.noValue')\n try {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return t('push_notifications.deliveries.noValue')\n return date.toLocaleString()\n } catch {\n return t('push_notifications.deliveries.noValue')\n }\n}\n\nexport default function PushDeliveriesListPage() {\n const [rows, setRows] = React.useState<Row[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [userOptions, setUserOptions] = React.useState<{ value: string; label: string; description?: string | null }[]>([])\n\n // Filter deliveries by recipient picked via name/email search instead of a raw UUID (mirrors the\n // devices list). Admins without auth.users.list degrade gracefully to no options.\n const loadUserOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) params.set('search', query.trim())\n const call = await apiCall<{ items?: { id: string; name?: string | null; email?: string | null }[] }>(\n `/api/auth/users?${params.toString()}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n ).catch(() => null)\n if (!call || !call.ok) return []\n const next = (call.result?.items ?? []).flatMap((item) => {\n if (!item || typeof item.id !== 'string' || !item.id.trim()) return []\n const name = typeof item.name === 'string' && item.name.trim() ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim() ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{ value: item.id, label, description: email && email !== label ? email : null }]\n })\n setUserOptions((prev) => {\n const map = new Map(prev.map((opt) => [opt.value, opt]))\n for (const opt of next) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n return next\n }, [])\n\n React.useEffect(() => { void loadUserOptions() }, [loadUserOptions, scopeVersion])\n\n // Reuse the picker cache to label the User column; rows whose owner isn't cached still show the id.\n const userLabelById = React.useMemo(\n () => new Map(userOptions.map((opt) => [opt.value, opt.label])),\n [userOptions],\n )\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('push_notifications.deliveries.columns.status'),\n type: 'select',\n options: [\n { value: 'pending', label: t('push_notifications.deliveries.status.pending') },\n { value: 'sending', label: t('push_notifications.deliveries.status.sending') },\n { value: 'sent', label: t('push_notifications.deliveries.status.sent') },\n { value: 'failed', label: t('push_notifications.deliveries.status.failed') },\n { value: 'skipped', label: t('push_notifications.deliveries.status.skipped') },\n { value: 'expired', label: t('push_notifications.deliveries.status.expired') },\n ],\n },\n {\n id: 'userId',\n label: t('push_notifications.deliveries.columns.user'),\n type: 'combobox',\n options: userOptions,\n loadOptions: loadUserOptions,\n },\n // A single created-at range picker (ISO `yyyy-MM-dd`) that maps to the ?from/?to query params.\n { id: 'createdRange', label: t('push_notifications.deliveries.filters.created'), type: 'dateRange' },\n ], [t, userOptions, loadUserOptions])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '50')\n const status = typeof filterValues.status === 'string' ? filterValues.status.trim() : ''\n const userId = typeof filterValues.userId === 'string' ? filterValues.userId.trim() : ''\n const createdRange = (filterValues.createdRange ?? {}) as { from?: string; to?: string }\n const from = typeof createdRange.from === 'string' ? createdRange.from.trim() : ''\n const to = typeof createdRange.to === 'string' ? createdRange.to.trim() : ''\n if (status) params.set('status', status)\n if (userId) params.set('userId', userId)\n if (from) params.set('from', from)\n if (to) params.set('to', to)\n const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(`/api/push_notifications/deliveries?${params.toString()}`, undefined, { fallback })\n if (!call.ok) {\n const errorPayload = call.result as { error?: string } | undefined\n const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('push_notifications.deliveries.error.loadFailed')\n flash(message, 'error')\n return\n }\n const payload = call.result ?? fallback\n if (!cancelled) {\n setRows(Array.isArray(payload.items) ? payload.items : [])\n setTotal(payload.total || 0)\n setTotalPages(payload.totalPages || 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n }\n } catch (error) {\n if (!cancelled) {\n const message = error instanceof Error ? error.message : t('push_notifications.deliveries.error.loadFailed')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [page, scopeVersion, filterValues, t])\n\n const columns = React.useMemo<ColumnDef<Row>[]>(() => [\n {\n accessorKey: 'status',\n header: t('push_notifications.deliveries.columns.status'),\n cell: ({ row }) => (\n <StatusBadge variant={statusVariant[row.original.status] ?? 'neutral'} dot>\n {t(`push_notifications.deliveries.status.${row.original.status}`)}\n </StatusBadge>\n ),\n },\n { accessorKey: 'notification_type_id', header: t('push_notifications.deliveries.columns.type') },\n {\n accessorKey: 'user_id',\n header: t('push_notifications.deliveries.columns.user'),\n cell: ({ row }) => {\n const userId = row.original.user_id\n const label = userLabelById.get(userId)\n return (\n // Stop the click bubbling to the row, whose default action opens the delivery detail.\n <Link\n href={`/backend/users/${encodeURIComponent(userId)}/edit`}\n className=\"text-primary hover:underline\"\n onClick={(e) => e.stopPropagation()}\n >\n {label ?? <code className=\"text-xs\">{userId}</code>}\n </Link>\n )\n },\n },\n { accessorKey: 'provider', header: t('push_notifications.deliveries.columns.provider') },\n {\n accessorKey: 'attempts',\n header: t('push_notifications.deliveries.columns.attempts'),\n },\n {\n accessorKey: 'created_at',\n header: t('push_notifications.deliveries.columns.created'),\n cell: ({ row }) => formatDate(row.original.created_at, t),\n },\n {\n accessorKey: 'sent_at',\n header: t('push_notifications.deliveries.columns.sent'),\n cell: ({ row }) => formatDate(row.original.sent_at, t),\n },\n ], [t, userLabelById])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('push_notifications.deliveries.title')}\n titleHeadingLevel={1}\n columns={columns}\n data={rows}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => { setFilterValues(values); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n perspective={{ tableId: 'push_notifications.deliveries' }}\n onRowClick={(row) => { window.location.href = `/backend/push_notifications/${row.id}` }}\n pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n emptyState={t('push_notifications.deliveries.empty')}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { formatDisplayDateTime } from '@open-mercato/ui/primitives/date-format'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype PushDeliveryStatus = 'pending' | 'sending' | 'sent' | 'failed' | 'skipped' | 'expired'\n\ntype Row = {\n id: string\n notification_type_id: string\n user_id: string\n provider: string\n token_snapshot: string\n status: PushDeliveryStatus\n attempts: number\n last_error: string | null\n created_at: string | null\n sent_at: string | null\n}\n\ntype ResponsePayload = {\n items: Row[]\n total: number\n page?: number\n pageSize?: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\nconst statusVariant: StatusMap<PushDeliveryStatus> = {\n pending: 'info',\n sending: 'info',\n sent: 'success',\n failed: 'error',\n skipped: 'neutral',\n expired: 'warning',\n}\n\nfunction formatDate(value: string | null, locale: string | undefined, t: (key: string) => string) {\n return formatDisplayDateTime(value, locale) ?? t('push_notifications.deliveries.noValue')\n}\n\nexport default function PushDeliveriesListPage() {\n const [rows, setRows] = React.useState<Row[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const dateLocale = useLocale()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [userOptions, setUserOptions] = React.useState<{ value: string; label: string; description?: string | null }[]>([])\n\n // Filter deliveries by recipient picked via name/email search instead of a raw UUID (mirrors the\n // devices list). Admins without auth.users.list degrade gracefully to no options.\n const loadUserOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) params.set('search', query.trim())\n const call = await apiCall<{ items?: { id: string; name?: string | null; email?: string | null }[] }>(\n `/api/auth/users?${params.toString()}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n ).catch(() => null)\n if (!call || !call.ok) return []\n const next = (call.result?.items ?? []).flatMap((item) => {\n if (!item || typeof item.id !== 'string' || !item.id.trim()) return []\n const name = typeof item.name === 'string' && item.name.trim() ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim() ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{ value: item.id, label, description: email && email !== label ? email : null }]\n })\n setUserOptions((prev) => {\n const map = new Map(prev.map((opt) => [opt.value, opt]))\n for (const opt of next) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n return next\n }, [])\n\n React.useEffect(() => { void loadUserOptions() }, [loadUserOptions, scopeVersion])\n\n // Reuse the picker cache to label the User column; rows whose owner isn't cached still show the id.\n const userLabelById = React.useMemo(\n () => new Map(userOptions.map((opt) => [opt.value, opt.label])),\n [userOptions],\n )\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('push_notifications.deliveries.columns.status'),\n type: 'select',\n options: [\n { value: 'pending', label: t('push_notifications.deliveries.status.pending') },\n { value: 'sending', label: t('push_notifications.deliveries.status.sending') },\n { value: 'sent', label: t('push_notifications.deliveries.status.sent') },\n { value: 'failed', label: t('push_notifications.deliveries.status.failed') },\n { value: 'skipped', label: t('push_notifications.deliveries.status.skipped') },\n { value: 'expired', label: t('push_notifications.deliveries.status.expired') },\n ],\n },\n {\n id: 'userId',\n label: t('push_notifications.deliveries.columns.user'),\n type: 'combobox',\n options: userOptions,\n loadOptions: loadUserOptions,\n },\n // A single created-at range picker (ISO `yyyy-MM-dd`) that maps to the ?from/?to query params.\n { id: 'createdRange', label: t('push_notifications.deliveries.filters.created'), type: 'dateRange' },\n ], [t, userOptions, loadUserOptions])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '50')\n const status = typeof filterValues.status === 'string' ? filterValues.status.trim() : ''\n const userId = typeof filterValues.userId === 'string' ? filterValues.userId.trim() : ''\n const createdRange = (filterValues.createdRange ?? {}) as { from?: string; to?: string }\n const from = typeof createdRange.from === 'string' ? createdRange.from.trim() : ''\n const to = typeof createdRange.to === 'string' ? createdRange.to.trim() : ''\n if (status) params.set('status', status)\n if (userId) params.set('userId', userId)\n if (from) params.set('from', from)\n if (to) params.set('to', to)\n const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(`/api/push_notifications/deliveries?${params.toString()}`, undefined, { fallback })\n if (!call.ok) {\n const errorPayload = call.result as { error?: string } | undefined\n const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('push_notifications.deliveries.error.loadFailed')\n flash(message, 'error')\n return\n }\n const payload = call.result ?? fallback\n if (!cancelled) {\n setRows(Array.isArray(payload.items) ? payload.items : [])\n setTotal(payload.total || 0)\n setTotalPages(payload.totalPages || 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n }\n } catch (error) {\n if (!cancelled) {\n const message = error instanceof Error ? error.message : t('push_notifications.deliveries.error.loadFailed')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [page, scopeVersion, filterValues, t])\n\n const columns = React.useMemo<ColumnDef<Row>[]>(() => [\n {\n accessorKey: 'status',\n header: t('push_notifications.deliveries.columns.status'),\n cell: ({ row }) => (\n <StatusBadge variant={statusVariant[row.original.status] ?? 'neutral'} dot>\n {t(`push_notifications.deliveries.status.${row.original.status}`)}\n </StatusBadge>\n ),\n },\n { accessorKey: 'notification_type_id', header: t('push_notifications.deliveries.columns.type') },\n {\n accessorKey: 'user_id',\n header: t('push_notifications.deliveries.columns.user'),\n cell: ({ row }) => {\n const userId = row.original.user_id\n const label = userLabelById.get(userId)\n return (\n // Stop the click bubbling to the row, whose default action opens the delivery detail.\n <Link\n href={`/backend/users/${encodeURIComponent(userId)}/edit`}\n className=\"text-primary hover:underline\"\n onClick={(e) => e.stopPropagation()}\n >\n {label ?? <code className=\"text-xs\">{userId}</code>}\n </Link>\n )\n },\n },\n { accessorKey: 'provider', header: t('push_notifications.deliveries.columns.provider') },\n {\n accessorKey: 'attempts',\n header: t('push_notifications.deliveries.columns.attempts'),\n },\n {\n accessorKey: 'created_at',\n header: t('push_notifications.deliveries.columns.created'),\n cell: ({ row }) => formatDate(row.original.created_at, dateLocale, t),\n },\n {\n accessorKey: 'sent_at',\n header: t('push_notifications.deliveries.columns.sent'),\n cell: ({ row }) => formatDate(row.original.sent_at, dateLocale, t),\n },\n ], [t, dateLocale, userLabelById])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('push_notifications.deliveries.title')}\n titleHeadingLevel={1}\n columns={columns}\n data={rows}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => { setFilterValues(values); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n perspective={{ tableId: 'push_notifications.deliveries' }}\n onRowClick={(row) => { window.location.href = `/backend/push_notifications/${row.id}` }}\n pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n emptyState={t('push_notifications.deliveries.empty')}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": ";AA+KQ;AA9KR,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,iBAAiB;AAE1B,SAAS,mBAAmC;AAC5C,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,mCAAmC;AAC5C,SAAS,MAAM,iBAAiB;AAChC,SAAS,6BAA6B;AA2BtC,MAAM,gBAA+C;AAAA,EACnD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAEA,SAAS,WAAW,OAAsB,QAA4B,GAA4B;AAChG,SAAO,sBAAsB,OAAO,MAAM,KAAK,EAAE,uCAAuC;AAC1F;AAEe,SAAR,yBAA0C;AAC/C,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAgB,CAAC,CAAC;AAChD,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,CAAC;AACxC,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,CAAC;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,eAAe,4BAA4B;AACjD,QAAM,IAAI,KAAK;AACf,QAAM,aAAa,UAAU;AAC7B,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAuB,CAAC,CAAC;AACvE,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAA0E,CAAC,CAAC;AAIxH,QAAM,kBAAkB,MAAM,YAAY,OAAO,UAAmB;AAClE,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,QAAQ,GAAG;AACtB,WAAO,IAAI,YAAY,IAAI;AAC3B,QAAI,SAAS,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,IAAI,UAAU,MAAM,KAAK,CAAC;AACvE,UAAM,OAAO,MAAM;AAAA,MACjB,mBAAmB,OAAO,SAAS,CAAC;AAAA,MACpC,EAAE,SAAS,EAAE,2BAA2B,IAAI,EAAE;AAAA,MAC9C,EAAE,UAAU,KAAK;AAAA,IACnB,EAAE,MAAM,MAAM,IAAI;AAClB,QAAI,CAAC,QAAQ,CAAC,KAAK,GAAI,QAAO,CAAC;AAC/B,UAAM,QAAQ,KAAK,QAAQ,SAAS,CAAC,GAAG,QAAQ,CAAC,SAAS;AACxD,UAAI,CAAC,QAAQ,OAAO,KAAK,OAAO,YAAY,CAAC,KAAK,GAAG,KAAK,EAAG,QAAO,CAAC;AACrE,YAAM,OAAO,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI;AACpF,YAAM,QAAQ,OAAO,KAAK,UAAU,YAAY,KAAK,MAAM,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI;AACxF,YAAM,QAAQ,QAAQ,SAAS,KAAK;AACpC,aAAO,CAAC,EAAE,OAAO,KAAK,IAAI,OAAO,aAAa,SAAS,UAAU,QAAQ,QAAQ,KAAK,CAAC;AAAA,IACzF,CAAC;AACD,mBAAe,CAAC,SAAS;AACvB,YAAM,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC;AACvD,iBAAW,OAAO,KAAM,KAAI,IAAI,IAAI,OAAO,GAAG;AAC9C,aAAO,MAAM,KAAK,IAAI,OAAO,CAAC;AAAA,IAChC,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AAAE,SAAK,gBAAgB;AAAA,EAAE,GAAG,CAAC,iBAAiB,YAAY,CAAC;AAGjF,QAAM,gBAAgB,MAAM;AAAA,IAC1B,MAAM,IAAI,IAAI,YAAY,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC;AAAA,IAC9D,CAAC,WAAW;AAAA,EACd;AAEA,QAAM,UAAU,MAAM,QAAqB,MAAM;AAAA,IAC/C;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,8CAA8C;AAAA,MACvD,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,OAAO,WAAW,OAAO,EAAE,8CAA8C,EAAE;AAAA,QAC7E,EAAE,OAAO,WAAW,OAAO,EAAE,8CAA8C,EAAE;AAAA,QAC7E,EAAE,OAAO,QAAQ,OAAO,EAAE,2CAA2C,EAAE;AAAA,QACvE,EAAE,OAAO,UAAU,OAAO,EAAE,6CAA6C,EAAE;AAAA,QAC3E,EAAE,OAAO,WAAW,OAAO,EAAE,8CAA8C,EAAE;AAAA,QAC7E,EAAE,OAAO,WAAW,OAAO,EAAE,8CAA8C,EAAE;AAAA,MAC/E;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,4CAA4C;AAAA,MACrD,MAAM;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA;AAAA,IAEA,EAAE,IAAI,gBAAgB,OAAO,EAAE,+CAA+C,GAAG,MAAM,YAAY;AAAA,EACrG,GAAG,CAAC,GAAG,aAAa,eAAe,CAAC;AAEpC,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,mBAAa,IAAI;AACjB,UAAI;AACF,cAAM,SAAS,IAAI,gBAAgB;AACnC,eAAO,IAAI,QAAQ,OAAO,IAAI,CAAC;AAC/B,eAAO,IAAI,YAAY,IAAI;AAC3B,cAAM,SAAS,OAAO,aAAa,WAAW,WAAW,aAAa,OAAO,KAAK,IAAI;AACtF,cAAM,SAAS,OAAO,aAAa,WAAW,WAAW,aAAa,OAAO,KAAK,IAAI;AACtF,cAAM,eAAgB,aAAa,gBAAgB,CAAC;AACpD,cAAM,OAAO,OAAO,aAAa,SAAS,WAAW,aAAa,KAAK,KAAK,IAAI;AAChF,cAAM,KAAK,OAAO,aAAa,OAAO,WAAW,aAAa,GAAG,KAAK,IAAI;AAC1E,YAAI,OAAQ,QAAO,IAAI,UAAU,MAAM;AACvC,YAAI,OAAQ,QAAO,IAAI,UAAU,MAAM;AACvC,YAAI,KAAM,QAAO,IAAI,QAAQ,IAAI;AACjC,YAAI,GAAI,QAAO,IAAI,MAAM,EAAE;AAC3B,cAAM,WAA4B,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,YAAY,EAAE;AAC7E,cAAM,OAAO,MAAM,QAAyB,sCAAsC,OAAO,SAAS,CAAC,IAAI,QAAW,EAAE,SAAS,CAAC;AAC9H,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,eAAe,KAAK;AAC1B,gBAAM,UAAU,OAAO,cAAc,UAAU,WAAW,aAAa,QAAQ,EAAE,gDAAgD;AACjI,gBAAM,SAAS,OAAO;AACtB;AAAA,QACF;AACA,cAAM,UAAU,KAAK,UAAU;AAC/B,YAAI,CAAC,WAAW;AACd,kBAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC,CAAC;AACzD,mBAAS,QAAQ,SAAS,CAAC;AAC3B,wBAAc,QAAQ,cAAc,CAAC;AACrC,2BAAiB,SAAS,kBAAkB,IAAI;AAAA,QAClD;AAAA,MACF,SAAS,OAAO;AACd,YAAI,CAAC,WAAW;AACd,gBAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,EAAE,gDAAgD;AAC3G,gBAAM,SAAS,OAAO;AAAA,QACxB;AAAA,MACF,UAAE;AACA,YAAI,CAAC,UAAW,cAAa,KAAK;AAAA,MACpC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,MAAM,cAAc,cAAc,CAAC,CAAC;AAExC,QAAM,UAAU,MAAM,QAA0B,MAAM;AAAA,IACpD;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,8CAA8C;AAAA,MACxD,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,eAAY,SAAS,cAAc,IAAI,SAAS,MAAM,KAAK,WAAW,KAAG,MACvE,YAAE,wCAAwC,IAAI,SAAS,MAAM,EAAE,GAClE;AAAA,IAEJ;AAAA,IACA,EAAE,aAAa,wBAAwB,QAAQ,EAAE,4CAA4C,EAAE;AAAA,IAC/F;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,4CAA4C;AAAA,MACtD,MAAM,CAAC,EAAE,IAAI,MAAM;AACjB,cAAM,SAAS,IAAI,SAAS;AAC5B,cAAM,QAAQ,cAAc,IAAI,MAAM;AACtC;AAAA;AAAA,UAEE;AAAA,YAAC;AAAA;AAAA,cACC,MAAM,kBAAkB,mBAAmB,MAAM,CAAC;AAAA,cAClD,WAAU;AAAA,cACV,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,cAEjC,mBAAS,oBAAC,UAAK,WAAU,WAAW,kBAAO;AAAA;AAAA,UAC9C;AAAA;AAAA,MAEJ;AAAA,IACF;AAAA,IACA,EAAE,aAAa,YAAY,QAAQ,EAAE,gDAAgD,EAAE;AAAA,IACvF;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,gDAAgD;AAAA,IAC5D;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,+CAA+C;AAAA,MACzD,MAAM,CAAC,EAAE,IAAI,MAAM,WAAW,IAAI,SAAS,YAAY,YAAY,CAAC;AAAA,IACtE;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,EAAE,4CAA4C;AAAA,MACtD,MAAM,CAAC,EAAE,IAAI,MAAM,WAAW,IAAI,SAAS,SAAS,YAAY,CAAC;AAAA,IACnE;AAAA,EACF,GAAG,CAAC,GAAG,YAAY,aAAa,CAAC;AAEjC,SACE,oBAAC,QACC,8BAAC,YACC;AAAA,IAAC;AAAA;AAAA,MACD,OAAO,EAAE,qCAAqC;AAAA,MAC9C,mBAAmB;AAAA,MACjB;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,gBAAgB,CAAC,WAAW;AAAE,wBAAgB,MAAM;AAAG,gBAAQ,CAAC;AAAA,MAAE;AAAA,MAClE,gBAAgB,MAAM;AAAE,wBAAgB,CAAC,CAAC;AAAG,gBAAQ,CAAC;AAAA,MAAE;AAAA,MACxD,aAAa,EAAE,SAAS,gCAAgC;AAAA,MACxD,YAAY,CAAC,QAAQ;AAAE,eAAO,SAAS,OAAO,+BAA+B,IAAI,EAAE;AAAA,MAAG;AAAA,MACtF,YAAY,EAAE,MAAM,UAAU,IAAI,OAAO,YAAY,eAAe,cAAc,QAAQ;AAAA,MAC1F;AAAA,MACA,YAAY,EAAE,qCAAqC;AAAA;AAAA,EACrD,GACF,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7874.1.67e097d23a",
3
+ "version": "0.10.1-develop.7883.1.44ac2125f5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.6.6"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7874.1.67e097d23a",
270
- "@open-mercato/shared": "0.10.1-develop.7874.1.67e097d23a",
271
- "@open-mercato/ui": "0.10.1-develop.7874.1.67e097d23a",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7883.1.44ac2125f5",
270
+ "@open-mercato/shared": "0.10.1-develop.7883.1.44ac2125f5",
271
+ "@open-mercato/ui": "0.10.1-develop.7883.1.44ac2125f5",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7874.1.67e097d23a",
277
- "@open-mercato/shared": "0.10.1-develop.7874.1.67e097d23a",
278
- "@open-mercato/ui": "0.10.1-develop.7874.1.67e097d23a",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7883.1.44ac2125f5",
277
+ "@open-mercato/shared": "0.10.1-develop.7883.1.44ac2125f5",
278
+ "@open-mercato/ui": "0.10.1-develop.7883.1.44ac2125f5",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -0,0 +1,87 @@
1
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
2
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
3
+ import { ATTACHMENT_TRANSFER_MAX_IDS } from './transferLimits'
4
+
5
+ export type TransferAttachmentsInput = {
6
+ entityId: string
7
+ attachmentIds: string[]
8
+ fromRecordId?: string | null
9
+ toRecordId: string
10
+ }
11
+
12
+ export type TransferAttachmentsResult = {
13
+ transferredCount: number
14
+ failedIds: string[]
15
+ unconfirmedCount: number
16
+ errors: string[]
17
+ }
18
+
19
+ export function chunkAttachmentIds(attachmentIds: string[], size = ATTACHMENT_TRANSFER_MAX_IDS): string[][] {
20
+ const chunks: string[][] = []
21
+ for (let index = 0; index < attachmentIds.length; index += size) {
22
+ chunks.push(attachmentIds.slice(index, index + size))
23
+ }
24
+ return chunks
25
+ }
26
+
27
+ function stopsRemainingChunks(error: unknown): boolean {
28
+ const name = typeof error === 'object' && error !== null ? (error as { name?: unknown }).name : undefined
29
+ return name === 'AbortError' || name === 'UnauthorizedError'
30
+ }
31
+
32
+ function describeError(error: unknown): string {
33
+ return error instanceof Error && error.message ? error.message : String(error)
34
+ }
35
+
36
+ export async function transferAttachmentsInChunks({
37
+ entityId,
38
+ attachmentIds,
39
+ fromRecordId,
40
+ toRecordId,
41
+ }: TransferAttachmentsInput): Promise<TransferAttachmentsResult> {
42
+ const result: TransferAttachmentsResult = { transferredCount: 0, failedIds: [], unconfirmedCount: 0, errors: [] }
43
+ const chunks = chunkAttachmentIds(attachmentIds)
44
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
45
+ const chunk = chunks[chunkIndex]
46
+ try {
47
+ const call = await apiCall<{ ok?: boolean; updated?: number; error?: string }>(
48
+ '/api/attachments/transfer',
49
+ {
50
+ method: 'POST',
51
+ headers: { 'content-type': 'application/json' },
52
+ body: JSON.stringify({
53
+ entityId,
54
+ attachmentIds: chunk,
55
+ ...(fromRecordId ? { fromRecordId } : {}),
56
+ toRecordId,
57
+ }),
58
+ },
59
+ { fallback: null },
60
+ )
61
+ if (!call.ok) {
62
+ result.failedIds.push(...chunk)
63
+ if (call.result?.error) result.errors.push(call.result.error)
64
+ continue
65
+ }
66
+ const updated = call.result?.updated
67
+ const confirmed = typeof updated === 'number' && Number.isFinite(updated)
68
+ ? Math.min(Math.max(Math.trunc(updated), 0), chunk.length)
69
+ : 0
70
+ result.transferredCount += confirmed
71
+ result.unconfirmedCount += chunk.length - confirmed
72
+ } catch (error) {
73
+ getTelemetryRuntime()?.reportError(error, {
74
+ module: 'attachments',
75
+ code: 'attachments.transfer_chunk_failed',
76
+ attributes: { chunkSize: chunk.length },
77
+ })
78
+ result.errors.push(describeError(error))
79
+ if (stopsRemainingChunks(error)) {
80
+ result.failedIds.push(...chunks.slice(chunkIndex).flat())
81
+ break
82
+ }
83
+ result.failedIds.push(...chunk)
84
+ }
85
+ }
86
+ return result
87
+ }
@@ -0,0 +1 @@
1
+ export const ATTACHMENT_TRANSFER_MAX_IDS = 100
@@ -35,10 +35,10 @@ import {
35
35
  VariantPricesSection,
36
36
  VariantMediaSection,
37
37
  } from '@open-mercato/core/modules/catalog/components/products/VariantBuilder'
38
- import type { ProductMediaItem } from '@open-mercato/core/modules/catalog/components/products/ProductMediaManager'
39
38
  import { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'
40
39
  import { fetchOptionSchemaTemplate } from '../../../optionSchemaClient'
41
40
  import { createLogger } from '@open-mercato/shared/lib/logger'
41
+ import { transferDraftMedia } from '@open-mercato/core/modules/catalog/components/products/mediaTransfer'
42
42
 
43
43
  const logger = createLogger('catalog')
44
44
 
@@ -393,11 +393,19 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
393
393
  if (!variantId) {
394
394
  throw createCrudFormError(t('catalog.variants.form.errors.idMissing', 'Variant id missing after create.'))
395
395
  }
396
- await transferVariantMedia({
396
+ const mediaTransfer = await transferDraftMedia({
397
+ entityId: E.catalog.catalog_product_variant,
397
398
  draftId: values.mediaDraftId,
398
- variantId,
399
+ recordId: variantId,
399
400
  mediaItems: Array.isArray(values.mediaItems) ? values.mediaItems : [],
400
401
  })
402
+ if (mediaTransfer.failedCount > 0) {
403
+ logger.error('attachments.transfer.failed', {
404
+ failed: mediaTransfer.failedCount,
405
+ requested: mediaTransfer.requestedCount,
406
+ errors: mediaTransfer.errors,
407
+ })
408
+ }
401
409
  await syncVariantPrices({
402
410
  priceKinds,
403
411
  priceDrafts: values.prices ?? {},
@@ -408,7 +416,18 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
408
416
  productTaxRateId,
409
417
  productTaxRate,
410
418
  })
411
- flash(t('catalog.variants.form.createSuccess', 'Variant created.'), 'success')
419
+ if (mediaTransfer.failedCount > 0) {
420
+ flash(
421
+ t(
422
+ 'catalog.variants.form.mediaTransferIncomplete',
423
+ 'Variant created, but {failed} of {total} media files could not be attached.',
424
+ { failed: mediaTransfer.failedCount, total: mediaTransfer.requestedCount },
425
+ ),
426
+ 'warning',
427
+ )
428
+ } else {
429
+ flash(t('catalog.variants.form.createSuccess', 'Variant created.'), 'success')
430
+ }
412
431
  router.push(`/backend/catalog/products/${productId}#variants`)
413
432
  }}
414
433
  />
@@ -417,34 +436,6 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
417
436
  )
418
437
  }
419
438
 
420
- async function transferVariantMedia({
421
- draftId,
422
- variantId,
423
- mediaItems,
424
- }: {
425
- draftId?: string | null
426
- variantId: string
427
- mediaItems: ProductMediaItem[]
428
- }): Promise<void> {
429
- if (!draftId || !variantId) return
430
- const attachmentIds = mediaItems.map((item) => item.id).filter((id): id is string => typeof id === 'string' && id.length > 0)
431
- if (!attachmentIds.length) return
432
- await apiCall(
433
- '/api/attachments/transfer',
434
- {
435
- method: 'POST',
436
- headers: { 'content-type': 'application/json' },
437
- body: JSON.stringify({
438
- entityId: E.catalog.catalog_product_variant,
439
- attachmentIds,
440
- fromRecordId: draftId,
441
- toRecordId: variantId,
442
- }),
443
- },
444
- { fallback: null },
445
- )
446
- }
447
-
448
439
  async function syncVariantPrices({
449
440
  priceKinds,
450
441
  priceDrafts,
@@ -98,6 +98,7 @@ import {
98
98
  type ProductUnitConversionInput,
99
99
  } from "@open-mercato/core/modules/catalog/components/products/productFormUtils";
100
100
  import { createLogger } from '@open-mercato/shared/lib/logger'
101
+ import { transferDraftMedia } from "@open-mercato/core/modules/catalog/components/products/mediaTransfer";
101
102
 
102
103
  const logger = createLogger('catalog')
103
104
 
@@ -419,9 +420,6 @@ export default function CreateCatalogProductPage() {
419
420
  const mediaItems = Array.isArray(formValues.mediaItems)
420
421
  ? formValues.mediaItems
421
422
  : [];
422
- const attachmentIds = mediaItems
423
- .map((item) => (typeof item.id === "string" ? item.id : null))
424
- .filter((value): value is string => !!value);
425
423
  const mediaDraftId =
426
424
  typeof formValues.mediaDraftId === "string"
427
425
  ? formValues.mediaDraftId
@@ -768,27 +766,18 @@ export default function CreateCatalogProductPage() {
768
766
  await createCrud("catalog/prices", pricePayload);
769
767
  }
770
768
 
771
- if (mediaDraftId && attachmentIds.length) {
772
- const transfer = await apiCall<{
773
- ok?: boolean;
774
- error?: string;
775
- }>(
776
- "/api/attachments/transfer",
777
- {
778
- method: "POST",
779
- headers: { "content-type": "application/json" },
780
- body: JSON.stringify({
781
- entityId: E.catalog.catalog_product,
782
- attachmentIds,
783
- fromRecordId: mediaDraftId,
784
- toRecordId: productId,
785
- }),
786
- },
787
- { fallback: null },
788
- );
789
- if (!transfer.ok) {
790
- logger.error("attachments.transfer.failed", { err: transfer.result?.error });
791
- }
769
+ const mediaTransfer = await transferDraftMedia({
770
+ entityId: E.catalog.catalog_product,
771
+ draftId: mediaDraftId,
772
+ recordId: productId,
773
+ mediaItems,
774
+ });
775
+ if (mediaTransfer.failedCount > 0) {
776
+ logger.error("attachments.transfer.failed", {
777
+ failed: mediaTransfer.failedCount,
778
+ requested: mediaTransfer.requestedCount,
779
+ errors: mediaTransfer.errors,
780
+ });
792
781
  }
793
782
 
794
783
  if (inboxDraft) {
@@ -817,10 +806,24 @@ export default function CreateCatalogProductPage() {
817
806
  }
818
807
  }
819
808
 
820
- flash(
821
- t("catalog.products.create.success", "Product created."),
822
- "success",
823
- );
809
+ if (mediaTransfer.failedCount > 0) {
810
+ flash(
811
+ t(
812
+ "catalog.products.create.mediaTransferIncomplete",
813
+ "Product created, but {failed} of {total} media files could not be attached.",
814
+ {
815
+ failed: mediaTransfer.failedCount,
816
+ total: mediaTransfer.requestedCount,
817
+ },
818
+ ),
819
+ "warning",
820
+ );
821
+ } else {
822
+ flash(
823
+ t("catalog.products.create.success", "Product created."),
824
+ "success",
825
+ );
826
+ }
824
827
  if (inboxDraft) {
825
828
  router.push(
826
829
  `/backend/inbox-ops/proposals/${encodeURIComponent(inboxDraft.proposalId)}`,
@@ -20,7 +20,8 @@ import type { FilterOption } from '@open-mercato/ui/backend/FilterOverlay'
20
20
  import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
21
21
  import { markdownToPlainText } from '@open-mercato/ui/backend/markdown/markdownToPlainText'
22
22
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
23
- import { useT } from '@open-mercato/shared/lib/i18n/context'
23
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
24
+ import { formatDisplayDateTime } from '@open-mercato/ui/primitives/date-format'
24
25
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
25
26
  import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
26
27
  import { E } from '#generated/entities.ids.generated'
@@ -92,11 +93,8 @@ type ProductsResponse = {
92
93
  const PAGE_SIZE = 25
93
94
  const ENTITY_ID = E.catalog.catalog_product
94
95
 
95
- function formatDate(value?: string): string {
96
- if (!value) return '—'
97
- const date = new Date(value)
98
- if (Number.isNaN(date.getTime())) return '—'
99
- return date.toLocaleDateString()
96
+ function formatDate(value: string | undefined, locale: string | undefined): string {
97
+ return formatDisplayDateTime(value, locale) ?? '—'
100
98
  }
101
99
 
102
100
  function renderOffers(offers: OfferInfo[] | undefined): React.ReactNode {
@@ -172,6 +170,7 @@ export default function ProductsDataTable({
172
170
  onSnapshotChange,
173
171
  }: ProductsDataTableProps = {}) {
174
172
  const t = useT()
173
+ const dateLocale = useLocale()
175
174
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
176
175
  const scopeVersion = useOrganizationScopeVersion()
177
176
  const [rows, setRows] = React.useState<ProductRow[]>([])
@@ -467,11 +466,11 @@ export default function ProductsDataTable({
467
466
  {
468
467
  accessorKey: 'updated_at',
469
468
  header: t('catalog.products.table.updatedAt'),
470
- cell: ({ row }) => <span className="text-xs text-muted-foreground">{formatDate(row.original.updated_at)}</span>,
469
+ cell: ({ row }) => <span className="text-xs text-muted-foreground">{formatDate(row.original.updated_at, dateLocale)}</span>,
471
470
  },
472
471
  ]
473
472
  return applyCustomFieldVisibility(base, customFieldDefs)
474
- }, [customFieldDefs, productTypeLabelMap, t])
473
+ }, [customFieldDefs, productTypeLabelMap, t, dateLocale])
475
474
 
476
475
  const handleSearchChange = React.useCallback((value: string) => {
477
476
  setSearch(value)
@@ -0,0 +1,40 @@
1
+ import { transferAttachmentsInChunks } from '@open-mercato/core/modules/attachments/lib/transferClient'
2
+ import type { ProductMediaItem } from './ProductMediaManager'
3
+
4
+ export type DraftMediaTransferInput = {
5
+ entityId: string
6
+ draftId?: string | null
7
+ recordId: string
8
+ mediaItems: ProductMediaItem[]
9
+ }
10
+
11
+ export type DraftMediaTransferResult = {
12
+ requestedCount: number
13
+ failedCount: number
14
+ errors: string[]
15
+ }
16
+
17
+ export async function transferDraftMedia({
18
+ entityId,
19
+ draftId,
20
+ recordId,
21
+ mediaItems,
22
+ }: DraftMediaTransferInput): Promise<DraftMediaTransferResult> {
23
+ const attachmentIds = mediaItems
24
+ .map((item) => item.id)
25
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
26
+ if (!draftId || !recordId || !attachmentIds.length) {
27
+ return { requestedCount: 0, failedCount: 0, errors: [] }
28
+ }
29
+ const result = await transferAttachmentsInChunks({
30
+ entityId,
31
+ attachmentIds,
32
+ fromRecordId: draftId,
33
+ toRecordId: recordId,
34
+ })
35
+ return {
36
+ requestedCount: attachmentIds.length,
37
+ failedCount: result.failedIds.length + result.unconfirmedCount,
38
+ errors: result.errors,
39
+ }
40
+ }
@@ -416,6 +416,7 @@
416
416
  "catalog.products.create.errors.variant": "Variante konnte nicht erstellt werden.",
417
417
  "catalog.products.create.fields.currency": "Hauptwährung",
418
418
  "catalog.products.create.handleHelp": "Handle wird für URLs verwendet und muss eindeutig sein.",
419
+ "catalog.products.create.mediaTransferIncomplete": "Produkt erstellt, aber {failed} von {total} Mediendateien konnten nicht angehängt werden.",
419
420
  "catalog.products.create.meta.description": "Verwalte Untertitel, Handle und Währung für deine Storefronts.",
420
421
  "catalog.products.create.meta.handleLabel": "Handle",
421
422
  "catalog.products.create.meta.title": "Produkt-Metadaten",
@@ -882,6 +883,7 @@
882
883
  "catalog.variants.form.isDefaultLabel": "Standardvariante",
883
884
  "catalog.variants.form.loading": "Formular wird geladen...",
884
885
  "catalog.variants.form.media": "Medien",
886
+ "catalog.variants.form.mediaTransferIncomplete": "Variante erstellt, aber {failed} von {total} Mediendateien konnten nicht angehängt werden.",
885
887
  "catalog.variants.form.nameLabel": "Name",
886
888
  "catalog.variants.form.namePlaceholder": "z. B. Blau / Klein",
887
889
  "catalog.variants.form.nameRequired": "Geben Sie den Variantennamen an.",
@@ -416,6 +416,7 @@
416
416
  "catalog.products.create.errors.variant": "Failed to create variant.",
417
417
  "catalog.products.create.fields.currency": "Primary currency",
418
418
  "catalog.products.create.handleHelp": "Handle is used for URLs and must be unique.",
419
+ "catalog.products.create.mediaTransferIncomplete": "Product created, but {failed} of {total} media files could not be attached.",
419
420
  "catalog.products.create.meta.description": "Manage subtitle, handle, and currency for storefronts.",
420
421
  "catalog.products.create.meta.handleLabel": "Handle",
421
422
  "catalog.products.create.meta.title": "Product meta",
@@ -882,6 +883,7 @@
882
883
  "catalog.variants.form.isDefaultLabel": "Default variant",
883
884
  "catalog.variants.form.loading": "Loading form...",
884
885
  "catalog.variants.form.media": "Media",
886
+ "catalog.variants.form.mediaTransferIncomplete": "Variant created, but {failed} of {total} media files could not be attached.",
885
887
  "catalog.variants.form.nameLabel": "Name",
886
888
  "catalog.variants.form.namePlaceholder": "e.g., Blue / Small",
887
889
  "catalog.variants.form.nameRequired": "Provide the variant name.",
@@ -416,6 +416,7 @@
416
416
  "catalog.products.create.errors.variant": "No se pudo crear la variante.",
417
417
  "catalog.products.create.fields.currency": "Moneda principal",
418
418
  "catalog.products.create.handleHelp": "El identificador se usa en URLs y debe ser único.",
419
+ "catalog.products.create.mediaTransferIncomplete": "Producto creado, pero no se pudieron adjuntar {failed} de {total} archivos multimedia.",
419
420
  "catalog.products.create.meta.description": "Administra el subtítulo, el identificador y la moneda para las vitrinas.",
420
421
  "catalog.products.create.meta.handleLabel": "Identificador",
421
422
  "catalog.products.create.meta.title": "Metadatos del producto",
@@ -882,6 +883,7 @@
882
883
  "catalog.variants.form.isDefaultLabel": "Variante predeterminada",
883
884
  "catalog.variants.form.loading": "Cargando formulario...",
884
885
  "catalog.variants.form.media": "Medios",
886
+ "catalog.variants.form.mediaTransferIncomplete": "Variante creada, pero no se pudieron adjuntar {failed} de {total} archivos multimedia.",
885
887
  "catalog.variants.form.nameLabel": "Nombre",
886
888
  "catalog.variants.form.namePlaceholder": "ej., Azul / Pequeño",
887
889
  "catalog.variants.form.nameRequired": "Ingrese el nombre de la variante.",
@@ -416,6 +416,7 @@
416
416
  "catalog.products.create.errors.variant": "변형 생성에 실패했습니다.",
417
417
  "catalog.products.create.fields.currency": "기본 통화",
418
418
  "catalog.products.create.handleHelp": "핸들은 URL에 사용되며 고유해야 합니다.",
419
+ "catalog.products.create.mediaTransferIncomplete": "상품이 생성되었지만 미디어 파일 {total}개 중 {failed}개를 첨부하지 못했습니다.",
419
420
  "catalog.products.create.meta.description": "스토어프론트에 사용할 부제목, 핸들, 통화를 관리합니다.",
420
421
  "catalog.products.create.meta.handleLabel": "핸들",
421
422
  "catalog.products.create.meta.title": "상품 메타 정보",
@@ -882,6 +883,7 @@
882
883
  "catalog.variants.form.isDefaultLabel": "기본 변형",
883
884
  "catalog.variants.form.loading": "폼을 불러오는 중...",
884
885
  "catalog.variants.form.media": "미디어",
886
+ "catalog.variants.form.mediaTransferIncomplete": "변형이 생성되었지만 미디어 파일 {total}개 중 {failed}개를 첨부하지 못했습니다.",
885
887
  "catalog.variants.form.nameLabel": "이름",
886
888
  "catalog.variants.form.namePlaceholder": "예: 블루 / 스몰",
887
889
  "catalog.variants.form.nameRequired": "변형 이름을 입력하세요.",
@@ -416,6 +416,7 @@
416
416
  "catalog.products.create.errors.variant": "Nie udało się utworzyć wariantu.",
417
417
  "catalog.products.create.fields.currency": "Główna waluta",
418
418
  "catalog.products.create.handleHelp": "Identyfikator jest używany w adresach URL i musi być unikalny.",
419
+ "catalog.products.create.mediaTransferIncomplete": "Produkt utworzony, ale nie udało się dołączyć {failed} z {total} plików multimedialnych.",
419
420
  "catalog.products.create.meta.description": "Zarządzaj podtytułem, identyfikatorem i walutą dla witryn.",
420
421
  "catalog.products.create.meta.handleLabel": "Identyfikator",
421
422
  "catalog.products.create.meta.title": "Metadane produktu",
@@ -882,6 +883,7 @@
882
883
  "catalog.variants.form.isDefaultLabel": "Domyślny wariant",
883
884
  "catalog.variants.form.loading": "Ładowanie formularza...",
884
885
  "catalog.variants.form.media": "Multimedia",
886
+ "catalog.variants.form.mediaTransferIncomplete": "Wariant utworzony, ale nie udało się dołączyć {failed} z {total} plików multimedialnych.",
885
887
  "catalog.variants.form.nameLabel": "Nazwa",
886
888
  "catalog.variants.form.namePlaceholder": "np. Niebieski / Mały",
887
889
  "catalog.variants.form.nameRequired": "Podaj nazwę wariantu.",
@@ -11,7 +11,8 @@ import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-
11
11
  import { Tag, type TagMap } from '@open-mercato/ui/primitives/tag'
12
12
  import { Button } from '@open-mercato/ui/primitives/button'
13
13
  import { Plus, GripVertical } from 'lucide-react'
14
- import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
15
+ import { formatDisplayDateTime } from '@open-mercato/ui/primitives/date-format'
15
16
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
16
17
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
17
18
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
@@ -117,6 +118,7 @@ function DragHandle({ id }: { id: string }) {
117
118
 
118
119
  export default function CustomerGroupsPage() {
119
120
  const t = useT()
121
+ const dateLocale = useLocale()
120
122
  const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()
121
123
  const { payload: backendChromePayload, isReady: backendChromeReady } = useBackendChrome()
122
124
  const canManage = backendChromeReady && hasFeature(backendChromePayload?.grantedFeatures, MANAGE_FEATURE)
@@ -375,10 +377,10 @@ export default function CustomerGroupsPage() {
375
377
  {
376
378
  accessorKey: 'updated_at',
377
379
  header: t('customer_groups.groups.list.columns.updatedAt', 'Updated'),
378
- cell: ({ row }) => (row.original.updated_at ? new Date(row.original.updated_at).toLocaleString() : '—'),
380
+ cell: ({ row }) => formatDisplayDateTime(row.original.updated_at, dateLocale) ?? '—',
379
381
  },
380
382
  ],
381
- [t, dragReorderEnabled],
383
+ [t, dateLocale, dragReorderEnabled],
382
384
  )
383
385
 
384
386
  const filterDefs = React.useMemo<FilterDef[]>(