@open-mercato/core 0.10.1-develop.7870.1.63e4c252d3 → 0.10.1-develop.7879.1.80d3740285
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js +7 -8
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/customer_groups/backend/customer-groups/page.js +5 -3
- package/dist/modules/customer_groups/backend/customer-groups/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +4 -4
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +41 -30
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/page.js +8 -13
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +7 -8
- package/src/modules/customer_groups/backend/customer-groups/page.tsx +5 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +4 -4
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +42 -30
- 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
|
-
|
|
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": ";
|
|
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.
|
|
3
|
+
"version": "0.10.1-develop.7879.1.80d3740285",
|
|
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.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7879.1.80d3740285",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7879.1.80d3740285",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7879.1.80d3740285",
|
|
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.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7879.1.80d3740285",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7879.1.80d3740285",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7879.1.80d3740285",
|
|
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",
|
|
@@ -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
|
|
96
|
-
|
|
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)
|
|
@@ -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
|
|
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[]>(
|
|
@@ -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
|
)
|
|
@@ -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(
|
|
@@ -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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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>
|