@open-mercato/core 0.10.1-develop.7762.1.657bb7236b → 0.10.1-develop.7764.1.7505d7340b

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 (37) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/profile/route.js +16 -2
  3. package/dist/modules/auth/api/profile/route.js.map +2 -2
  4. package/dist/modules/auth/lib/userDataSubject.js +26 -0
  5. package/dist/modules/auth/lib/userDataSubject.js.map +7 -0
  6. package/dist/modules/customers/api/companies/route.js +2 -1
  7. package/dist/modules/customers/api/companies/route.js.map +2 -2
  8. package/dist/modules/customers/api/people/route.js +2 -1
  9. package/dist/modules/customers/api/people/route.js.map +2 -2
  10. package/dist/modules/customers/backend/customers/companies/page.js +21 -3
  11. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  12. package/dist/modules/customers/backend/customers/listSorting.js +2 -1
  13. package/dist/modules/customers/backend/customers/listSorting.js.map +2 -2
  14. package/dist/modules/customers/backend/customers/people/page.js +17 -1
  15. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  16. package/dist/modules/customers/lib/createdAtDayFilter.js +64 -0
  17. package/dist/modules/customers/lib/createdAtDayFilter.js.map +7 -0
  18. package/dist/modules/dashboards/api/layout/[itemId]/route.js +11 -4
  19. package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
  20. package/dist/modules/dashboards/api/layout/route.js +25 -13
  21. package/dist/modules/dashboards/api/layout/route.js.map +2 -2
  22. package/package.json +7 -7
  23. package/src/modules/auth/api/profile/route.ts +15 -1
  24. package/src/modules/auth/lib/userDataSubject.ts +30 -0
  25. package/src/modules/customers/api/companies/route.ts +2 -1
  26. package/src/modules/customers/api/people/route.ts +2 -1
  27. package/src/modules/customers/backend/customers/companies/page.tsx +26 -3
  28. package/src/modules/customers/backend/customers/listSorting.ts +1 -0
  29. package/src/modules/customers/backend/customers/people/page.tsx +22 -1
  30. package/src/modules/customers/i18n/de.json +2 -0
  31. package/src/modules/customers/i18n/en.json +2 -0
  32. package/src/modules/customers/i18n/es.json +2 -0
  33. package/src/modules/customers/i18n/ko.json +2 -0
  34. package/src/modules/customers/i18n/pl.json +2 -0
  35. package/src/modules/customers/lib/createdAtDayFilter.ts +68 -0
  36. package/src/modules/dashboards/api/layout/[itemId]/route.ts +12 -4
  37. package/src/modules/dashboards/api/layout/route.ts +25 -12
@@ -9,6 +9,8 @@ import { resolveAllowedWidgetIds } from "@open-mercato/core/modules/dashboards/l
9
9
  import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
10
10
  import { User } from "@open-mercato/core/modules/auth/data/entities";
11
11
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
12
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
13
+ import { resolveUserDataSubject } from "@open-mercato/core/modules/auth/lib/userDataSubject";
12
14
  import {
13
15
  runCrudMutationGuardAfterSuccess,
14
16
  validateCrudMutationGuard
@@ -61,13 +63,17 @@ async function GET(req) {
61
63
  const container = await createRequestContainer();
62
64
  const em = container.resolve("em").fork({ clear: true, freshEventManager: true, useContext: true });
63
65
  const rbac = container.resolve("rbacService");
64
- const url = new URL(req.url);
66
+ const userId = await resolveUserDataSubject(em, auth);
67
+ if (!userId) {
68
+ const { translate } = await resolveTranslations();
69
+ return NextResponse.json({ error: translate("api.errors.forbidden", "Forbidden") }, { status: 403 });
70
+ }
65
71
  const scope = {
66
- userId: String(auth.sub),
72
+ userId,
67
73
  tenantId: auth.tenantId ?? null,
68
74
  organizationId: auth.orgId ?? null
69
75
  };
70
- const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {
76
+ const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {
71
77
  tenantId: scope.tenantId,
72
78
  organizationId: scope.organizationId
73
79
  });
@@ -98,7 +104,7 @@ async function GET(req) {
98
104
  size: widget.metadata.defaultSize ?? DEFAULT_SIZE,
99
105
  settings: widget.metadata.defaultSettings ?? void 0
100
106
  }));
101
- if (hasRegisteredWidgets) {
107
+ if (hasRegisteredWidgets && !auth.isApiKey) {
102
108
  layout = em.create(DashboardLayout, {
103
109
  userId: scope.userId,
104
110
  tenantId: scope.tenantId,
@@ -108,7 +114,7 @@ async function GET(req) {
108
114
  em.persist(layout);
109
115
  hasChanged = true;
110
116
  }
111
- } else if (hasRegisteredWidgets) {
117
+ } else if (hasRegisteredWidgets || auth.isApiKey) {
112
118
  const existingLayout = layout;
113
119
  const filtered = items.filter((item) => allowedIds.includes(item.widgetId));
114
120
  if (filtered.length !== items.length) {
@@ -119,10 +125,10 @@ async function GET(req) {
119
125
  if (existingLayout.layoutJson.length !== items.length || items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)) {
120
126
  hasChanged = true;
121
127
  }
122
- existingLayout.layoutJson = items;
128
+ if (!auth.isApiKey) existingLayout.layoutJson = items;
123
129
  layout = existingLayout;
124
130
  }
125
- if (hasChanged) {
131
+ if (hasChanged && !auth.isApiKey) {
126
132
  await em.flush();
127
133
  }
128
134
  const canConfigure = authorizeFeatures(["dashboards.configure"], {
@@ -189,13 +195,18 @@ async function PUT(req) {
189
195
  const { resolve } = container;
190
196
  const em = resolve("em").fork({ clear: true, freshEventManager: true, useContext: true });
191
197
  const rbac = resolve("rbacService");
198
+ const userId = await resolveUserDataSubject(em, auth);
199
+ if (!userId) {
200
+ const { translate } = await resolveTranslations();
201
+ return NextResponse.json({ error: translate("api.errors.forbidden", "Forbidden") }, { status: 403 });
202
+ }
192
203
  const scope = {
193
- userId: String(auth.sub),
204
+ userId,
194
205
  tenantId: auth.tenantId ?? null,
195
206
  organizationId: auth.orgId ?? null
196
207
  };
197
208
  const canConfigure = await rbac.userHasAllFeatures(
198
- scope.userId,
209
+ auth.sub,
199
210
  ["dashboards.configure"],
200
211
  { tenantId: scope.tenantId, organizationId: scope.organizationId }
201
212
  );
@@ -205,7 +216,7 @@ async function PUT(req) {
205
216
  const guardResult = await validateCrudMutationGuard(container, {
206
217
  tenantId: scope.tenantId ?? "",
207
218
  organizationId: scope.organizationId,
208
- userId: scope.userId,
219
+ userId: auth.sub,
209
220
  resourceKind: RESOURCE_KIND,
210
221
  resourceId: scope.userId,
211
222
  operation: "update",
@@ -220,7 +231,7 @@ async function PUT(req) {
220
231
  if (widgets.length === 0) {
221
232
  return NextResponse.json({ error: "Widget registry unavailable" }, { status: 503 });
222
233
  }
223
- const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {
234
+ const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {
224
235
  tenantId: scope.tenantId,
225
236
  organizationId: scope.organizationId
226
237
  });
@@ -266,7 +277,7 @@ async function PUT(req) {
266
277
  await runCrudMutationGuardAfterSuccess(container, {
267
278
  tenantId: scope.tenantId ?? "",
268
279
  organizationId: scope.organizationId,
269
- userId: scope.userId,
280
+ userId: auth.sub,
270
281
  resourceKind: RESOURCE_KIND,
271
282
  resourceId: scope.userId,
272
283
  operation: "update",
@@ -289,7 +300,8 @@ const layoutGetDoc = {
289
300
  }
290
301
  ],
291
302
  errors: [
292
- { status: 401, description: "Authentication required", schema: dashboardsErrorSchema }
303
+ { status: 401, description: "Authentication required", schema: dashboardsErrorSchema },
304
+ { status: 403, description: "No user binding in the authenticated scope", schema: dashboardsErrorSchema }
293
305
  ]
294
306
  };
295
307
  const layoutPutDoc = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/dashboards/api/layout/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { randomUUID } from 'node:crypto'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutStateSchema,\n} from '../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },\n PUT: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\ntype LayoutScope = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nasync function loadScopeLayout(em: any, scope: LayoutScope): Promise<DashboardLayout | null> {\n return await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n}\n\nfunction normalizeLayoutItems(items: any[]) {\n const list = Array.isArray(items) ? items : []\n const seenIds = new Set<string>()\n const sanitized = list\n .filter((item) => item && typeof item === 'object')\n .map((item) => ({\n id: String(item.id),\n widgetId: String(item.widgetId),\n order: Number.isInteger(item.order) ? Number(item.order) : undefined,\n priority: Number.isInteger(item.priority) ? Number(item.priority) : undefined,\n size: typeof item.size === 'string' ? item.size : undefined,\n settings: item.settings,\n }))\n .filter((item) => {\n if (!item.id || !item.widgetId) return false\n if (seenIds.has(item.id)) return false\n seenIds.add(item.id)\n return true\n })\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, idx) => ({ ...item, order: idx, priority: idx }))\n return sanitized\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n // Use a fresh fork to avoid carrying over pending entities from other operations\n const em = (container.resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = container.resolve('rbacService') as any\n const url = new URL(req.url)\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n // An empty widget registry means the module registry has not been populated yet\n // (a boot race), not that this app has no widgets. Every write below is derived\n // from it, so a read request must persist nothing until it recovers \u2014 otherwise a\n // restart-recoverable glitch is written into saved layouts for good (#5041).\n // Note this keys off the registry, not off `allowedIds`: a user whose allowlist is\n // legitimately empty on a healthy registry is still pruned and seeded as before.\n const hasRegisteredWidgets = widgets.length > 0\n\n let layout = await loadScopeLayout(em, scope)\n let items = layout ? normalizeLayoutItems(layout.layoutJson) : []\n let hasChanged = false\n\n if (!layout) {\n const defaults = allowedWidgets.filter((widget) => widget.metadata.defaultEnabled)\n items = defaults.map((widget, index) => ({\n id: randomUUID(),\n widgetId: widget.metadata.id,\n order: index,\n priority: index,\n size: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n settings: widget.metadata.defaultSettings ?? undefined,\n }))\n if (hasRegisteredWidgets) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: items,\n })\n em.persist(layout)\n hasChanged = true\n }\n } else if (hasRegisteredWidgets) {\n const existingLayout = layout\n const filtered = items.filter((item) => allowedIds.includes(item.widgetId))\n if (filtered.length !== items.length) {\n hasChanged = true\n items = filtered\n }\n items = items.map((item, index) => (item.order !== index || item.priority !== index ? { ...item, order: index, priority: index } : item))\n if (\n existingLayout.layoutJson.length !== items.length ||\n items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)\n ) {\n hasChanged = true\n }\n existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged) {\n await em.flush()\n }\n\n const canConfigure = authorizeFeatures(['dashboards.configure'], {\n grantedFeatures: effectiveFeatures,\n })\n\n let userEmail: string | null = null\n let userName: string | null = null\n let userLabel: string | null = null\n const user = await findOneWithDecryption(\n em,\n User,\n { id: scope.userId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n )\n if (user) {\n userName = user.name?.trim() ?? null\n userEmail = user.email ?? null\n userLabel = (userName && userName.length > 0 ? userName : userEmail) ?? null\n }\n if (!userLabel) {\n userLabel = scope.userId\n }\n\n const response = {\n layout: { items },\n allowedWidgetIds: allowedIds,\n canConfigure,\n context: {\n ...scope,\n userName,\n userEmail,\n userLabel,\n },\n widgets: allowedWidgets.map((widget) => ({\n id: widget.metadata.id,\n title: widget.metadata.title,\n description: widget.metadata.description ?? null,\n defaultSize: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n defaultEnabled: !!widget.metadata.defaultEnabled,\n defaultSettings: widget.metadata.defaultSettings ?? null,\n features: widget.metadata.features ?? [],\n moduleId: widget.moduleId,\n icon: widget.metadata.icon ?? null,\n loaderKey: widget.key,\n supportsRefresh: !!widget.metadata.supportsRefresh,\n })),\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = dashboardLayoutSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = resolve('rbacService') as any\n\n const scope: LayoutScope = {\n userId: String(auth.sub),\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const canConfigure = await rbac.userHasAllFeatures(\n scope.userId,\n ['dashboards.configure'],\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!canConfigure) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: parsed.data.items },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const widgets = await loadAllWidgets()\n // The allowlist below is derived from the registry, and this handler persists the\n // filtered result unconditionally. An empty registry \u2014 the boot race behind #5041 \u2014\n // would drop every submitted item and save `[]` while answering `{ ok: true }`, so a\n // stale tab reordering a widget could erase the layout it just rendered. Fail the\n // write instead: the save is explicit, so the client must learn it did not happen\n // and retry, rather than be told a wipe succeeded.\n if (widgets.length === 0) {\n return NextResponse.json({ error: 'Widget registry unavailable' }, { status: 503 })\n }\n const effectiveFeatures = await rbac.getEffectiveFeatures(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedSet = new Set(allowedIds)\n\n const payloadItems = parsed.data.items\n const sanitized = payloadItems\n .map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings,\n }))\n .filter((item) => allowedSet.has(item.widgetId))\n\n const uniqueIds = new Set(sanitized.map((item) => item.id))\n if (uniqueIds.size !== sanitized.length) {\n return NextResponse.json({ error: 'Layout item IDs must be unique' }, { status: 400 })\n }\n\n let layout = await loadScopeLayout(em, scope)\n if (!layout) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: sanitized,\n })\n em.persist(layout)\n } else {\n layout.layoutJson = sanitized\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutGetDoc: OpenApiMethodDoc = {\n summary: 'Load the current dashboard layout',\n description: 'Returns the saved widget layout together with the widgets the current user is allowed to place.',\n tags: [dashboardsTag],\n responses: [\n {\n status: 200,\n description: 'Current dashboard layout and available widgets.',\n schema: dashboardLayoutStateSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n ],\n}\n\nconst layoutPutDoc: OpenApiMethodDoc = {\n summary: 'Persist dashboard layout changes',\n description: 'Saves the provided widget ordering, sizes, and settings for the current user.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutSchema,\n description: 'List of dashboard widgets with ordering, sizing, and settings.',\n },\n responses: [\n { status: 200, description: 'Layout updated successfully.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid layout payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n { status: 503, description: 'Widget registry unavailable \u2014 the layout was not saved', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage personal dashboard layout',\n methods: {\n GET: layoutGetDoc,\n PUT: layoutPutDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { randomUUID } from 'node:crypto'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'\nimport { dashboardLayoutSchema } from '@open-mercato/core/modules/dashboards/data/validators'\nimport { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'\nimport { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveUserDataSubject } from '@open-mercato/core/modules/auth/lib/userDataSubject'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n dashboardsTag,\n dashboardsErrorSchema,\n dashboardsOkSchema,\n dashboardLayoutStateSchema,\n} from '../openapi'\n\nconst DEFAULT_SIZE = 'md'\nconst RESOURCE_KIND = 'dashboards.layout'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },\n PUT: { requireAuth: true, requireFeatures: ['dashboards.configure'] },\n}\n\ntype LayoutScope = {\n userId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nasync function loadScopeLayout(em: any, scope: LayoutScope): Promise<DashboardLayout | null> {\n return await em.findOne(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n}\n\nfunction normalizeLayoutItems(items: any[]) {\n const list = Array.isArray(items) ? items : []\n const seenIds = new Set<string>()\n const sanitized = list\n .filter((item) => item && typeof item === 'object')\n .map((item) => ({\n id: String(item.id),\n widgetId: String(item.widgetId),\n order: Number.isInteger(item.order) ? Number(item.order) : undefined,\n priority: Number.isInteger(item.priority) ? Number(item.priority) : undefined,\n size: typeof item.size === 'string' ? item.size : undefined,\n settings: item.settings,\n }))\n .filter((item) => {\n if (!item.id || !item.widgetId) return false\n if (seenIds.has(item.id)) return false\n seenIds.add(item.id)\n return true\n })\n .sort((a, b) => {\n const aOrder = a.order ?? a.priority ?? 0\n const bOrder = b.order ?? b.priority ?? 0\n return aOrder - bOrder\n })\n .map((item, idx) => ({ ...item, order: idx, priority: idx }))\n return sanitized\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n // Use a fresh fork to avoid carrying over pending entities from other operations\n const em = (container.resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = container.resolve('rbacService') as any\n const userId = await resolveUserDataSubject(em, auth)\n if (!userId) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n\n const scope: LayoutScope = {\n userId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const widgets = await loadAllWidgets()\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedWidgets = widgets.filter((w) => allowedIds.includes(w.metadata.id))\n\n // An empty widget registry means the module registry has not been populated yet\n // (a boot race), not that this app has no widgets. Every write below is derived\n // from it, so a read request must persist nothing until it recovers \u2014 otherwise a\n // restart-recoverable glitch is written into saved layouts for good (#5041).\n // Note this keys off the registry, not off `allowedIds`: a user whose allowlist is\n // legitimately empty on a healthy registry is still pruned and seeded as before.\n const hasRegisteredWidgets = widgets.length > 0\n\n let layout = await loadScopeLayout(em, scope)\n let items = layout ? normalizeLayoutItems(layout.layoutJson) : []\n let hasChanged = false\n\n if (!layout) {\n const defaults = allowedWidgets.filter((widget) => widget.metadata.defaultEnabled)\n items = defaults.map((widget, index) => ({\n id: randomUUID(),\n widgetId: widget.metadata.id,\n order: index,\n priority: index,\n size: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n settings: widget.metadata.defaultSettings ?? undefined,\n }))\n if (hasRegisteredWidgets && !auth.isApiKey) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: items,\n })\n em.persist(layout)\n hasChanged = true\n }\n } else if (hasRegisteredWidgets || auth.isApiKey) {\n const existingLayout = layout\n const filtered = items.filter((item) => allowedIds.includes(item.widgetId))\n if (filtered.length !== items.length) {\n hasChanged = true\n items = filtered\n }\n items = items.map((item, index) => (item.order !== index || item.priority !== index ? { ...item, order: index, priority: index } : item))\n if (\n existingLayout.layoutJson.length !== items.length ||\n items.some((item, idx) => existingLayout.layoutJson[idx]?.id !== item.id)\n ) {\n hasChanged = true\n }\n if (!auth.isApiKey) existingLayout.layoutJson = items\n layout = existingLayout\n }\n\n if (hasChanged && !auth.isApiKey) {\n await em.flush()\n }\n\n const canConfigure = authorizeFeatures(['dashboards.configure'], {\n grantedFeatures: effectiveFeatures,\n })\n\n let userEmail: string | null = null\n let userName: string | null = null\n let userLabel: string | null = null\n const user = await findOneWithDecryption(\n em,\n User,\n { id: scope.userId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n )\n if (user) {\n userName = user.name?.trim() ?? null\n userEmail = user.email ?? null\n userLabel = (userName && userName.length > 0 ? userName : userEmail) ?? null\n }\n if (!userLabel) {\n userLabel = scope.userId\n }\n\n const response = {\n layout: { items },\n allowedWidgetIds: allowedIds,\n canConfigure,\n context: {\n ...scope,\n userName,\n userEmail,\n userLabel,\n },\n widgets: allowedWidgets.map((widget) => ({\n id: widget.metadata.id,\n title: widget.metadata.title,\n description: widget.metadata.description ?? null,\n defaultSize: widget.metadata.defaultSize ?? DEFAULT_SIZE,\n defaultEnabled: !!widget.metadata.defaultEnabled,\n defaultSettings: widget.metadata.defaultSettings ?? null,\n features: widget.metadata.features ?? [],\n moduleId: widget.moduleId,\n icon: widget.metadata.icon ?? null,\n loaderKey: widget.key,\n supportsRefresh: !!widget.metadata.supportsRefresh,\n })),\n }\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = dashboardLayoutSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })\n const rbac = resolve('rbacService') as any\n\n const userId = await resolveUserDataSubject(em, auth)\n if (!userId) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n\n const scope: LayoutScope = {\n userId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n\n const canConfigure = await rbac.userHasAllFeatures(\n auth.sub,\n ['dashboards.configure'],\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!canConfigure) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { items: parsed.data.items },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const widgets = await loadAllWidgets()\n // The allowlist below is derived from the registry, and this handler persists the\n // filtered result unconditionally. An empty registry \u2014 the boot race behind #5041 \u2014\n // would drop every submitted item and save `[]` while answering `{ ok: true }`, so a\n // stale tab reordering a widget could erase the layout it just rendered. Fail the\n // write instead: the save is explicit, so the client must learn it did not happen\n // and retry, rather than be told a wipe succeeded.\n if (widgets.length === 0) {\n return NextResponse.json({ error: 'Widget registry unavailable' }, { status: 503 })\n }\n const effectiveFeatures = await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const allowedIds = await resolveAllowedWidgetIds(\n em,\n {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n features: effectiveFeatures,\n isSuperAdmin: false,\n },\n widgets,\n )\n const allowedSet = new Set(allowedIds)\n\n const payloadItems = parsed.data.items\n const sanitized = payloadItems\n .map((item, index) => ({\n id: item.id,\n widgetId: item.widgetId,\n order: index,\n priority: index,\n size: item.size ?? DEFAULT_SIZE,\n settings: item.settings,\n }))\n .filter((item) => allowedSet.has(item.widgetId))\n\n const uniqueIds = new Set(sanitized.map((item) => item.id))\n if (uniqueIds.size !== sanitized.length) {\n return NextResponse.json({ error: 'Layout item IDs must be unique' }, { status: 400 })\n }\n\n let layout = await loadScopeLayout(em, scope)\n if (!layout) {\n layout = em.create(DashboardLayout, {\n userId: scope.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n layoutJson: sanitized,\n })\n em.persist(layout)\n } else {\n layout.layoutJson = sanitized\n }\n await em.flush()\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: scope.tenantId ?? '',\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: RESOURCE_KIND,\n resourceId: scope.userId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nconst layoutGetDoc: OpenApiMethodDoc = {\n summary: 'Load the current dashboard layout',\n description: 'Returns the saved widget layout together with the widgets the current user is allowed to place.',\n tags: [dashboardsTag],\n responses: [\n {\n status: 200,\n description: 'Current dashboard layout and available widgets.',\n schema: dashboardLayoutStateSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'No user binding in the authenticated scope', schema: dashboardsErrorSchema },\n ],\n}\n\nconst layoutPutDoc: OpenApiMethodDoc = {\n summary: 'Persist dashboard layout changes',\n description: 'Saves the provided widget ordering, sizes, and settings for the current user.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: dashboardLayoutSchema,\n description: 'List of dashboard widgets with ordering, sizing, and settings.',\n },\n responses: [\n { status: 200, description: 'Layout updated successfully.', schema: dashboardsOkSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid layout payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing dashboards.configure feature', schema: dashboardsErrorSchema },\n { status: 503, description: 'Widget registry unavailable \u2014 the layout was not saved', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Manage personal dashboard layout',\n methods: {\n GET: layoutGetDoc,\n PUT: layoutPutDoc,\n },\n}\n"],
5
+ "mappings": "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",
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.7762.1.657bb7236b",
3
+ "version": "0.10.1-develop.7764.1.7505d7340b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.4.3"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7762.1.657bb7236b",
270
- "@open-mercato/shared": "0.10.1-develop.7762.1.657bb7236b",
271
- "@open-mercato/ui": "0.10.1-develop.7762.1.657bb7236b",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7764.1.7505d7340b",
270
+ "@open-mercato/shared": "0.10.1-develop.7764.1.7505d7340b",
271
+ "@open-mercato/ui": "0.10.1-develop.7764.1.7505d7340b",
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.7762.1.657bb7236b",
277
- "@open-mercato/shared": "0.10.1-develop.7762.1.657bb7236b",
278
- "@open-mercato/ui": "0.10.1-develop.7762.1.657bb7236b",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7764.1.7505d7340b",
277
+ "@open-mercato/shared": "0.10.1-develop.7764.1.7505d7340b",
278
+ "@open-mercato/ui": "0.10.1-develop.7764.1.7505d7340b",
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",
@@ -14,6 +14,7 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
14
14
  import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'
15
15
  import { createLogger } from '@open-mercato/shared/lib/logger'
16
16
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
17
+ import { resolveUserDataSubject } from '@open-mercato/core/modules/auth/lib/userDataSubject'
17
18
 
18
19
  const logger = createLogger('auth').child({ component: 'profile' })
19
20
 
@@ -98,10 +99,18 @@ export async function GET(req: Request) {
98
99
  try {
99
100
  const container = await createRequestContainer()
100
101
  const em = (container.resolve('em') as EntityManager)
102
+ const userId = await resolveUserDataSubject(em, auth)
103
+ if (!userId) {
104
+ return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })
105
+ }
101
106
  const user = await findOneWithDecryption(
102
107
  em,
103
108
  User,
104
- { id: auth.sub, deletedAt: null },
109
+ {
110
+ id: userId,
111
+ deletedAt: null,
112
+ ...(auth.isApiKey ? { tenantId: auth.tenantId, ...(auth.orgId ? { organizationId: auth.orgId } : {}) } : {}),
113
+ },
105
114
  undefined,
106
115
  { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },
107
116
  )
@@ -126,6 +135,9 @@ export async function PUT(req: Request) {
126
135
  if (!auth?.sub) {
127
136
  return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
128
137
  }
138
+ if (auth.isApiKey) {
139
+ return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })
140
+ }
129
141
  try {
130
142
  const body = await req.json().catch(() => ({}))
131
143
  const parsed = buildUpdateSchema(translate).safeParse(body)
@@ -222,6 +234,7 @@ export const openApi: OpenApiRouteDoc = {
222
234
  responses: [
223
235
  { status: 200, description: 'Profile payload', schema: profileResponseSchema },
224
236
  { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
237
+ { status: 403, description: 'No user binding in the authenticated scope', schema: z.object({ error: z.string() }) },
225
238
  { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },
226
239
  ],
227
240
  },
@@ -236,6 +249,7 @@ export const openApi: OpenApiRouteDoc = {
236
249
  { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },
237
250
  { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
238
251
  { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
252
+ { status: 403, description: 'Interactive session required', schema: z.object({ error: z.string() }) },
239
253
  ],
240
254
  },
241
255
  },
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { User } from '../data/entities'
6
+
7
+ const uuidSchema = z.string().uuid()
8
+
9
+ export async function resolveUserDataSubject(
10
+ em: EntityManager,
11
+ auth: NonNullable<AuthContext>,
12
+ ): Promise<string | null> {
13
+ if (!auth.isApiKey) return auth.sub
14
+ if (!uuidSchema.safeParse(auth.userId).success || !uuidSchema.safeParse(auth.tenantId).success) return null
15
+ if (auth.orgId !== null && !uuidSchema.safeParse(auth.orgId).success) return null
16
+
17
+ const user = await findOneWithDecryption(
18
+ em,
19
+ User,
20
+ {
21
+ id: auth.userId,
22
+ tenantId: auth.tenantId,
23
+ ...(auth.orgId ? { organizationId: auth.orgId } : {}),
24
+ deletedAt: null,
25
+ },
26
+ undefined,
27
+ { tenantId: auth.tenantId, organizationId: auth.orgId },
28
+ )
29
+ return user?.id ?? null
30
+ }
@@ -28,6 +28,7 @@ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
28
28
  import { isUuidListParam, splitIdListParam } from '@open-mercato/shared/lib/crud/ids'
29
29
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
30
30
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
31
+ import { expandCreatedAtDayRules } from '../../lib/createdAtDayFilter'
31
32
  import {
32
33
  createCustomersCrudOpenApi,
33
34
  createPagedListResponseSchema,
@@ -135,7 +136,7 @@ const crud = makeCrudRoute({
135
136
  updatedAt: 'updated_at',
136
137
  },
137
138
  buildFilters: async (query, ctx) => {
138
- const advancedFilterTree = consumeAdvancedFilterState(query)
139
+ const advancedFilterTree = expandCreatedAtDayRules(consumeAdvancedFilterState(query), 'UTC')
139
140
  const filters: Record<string, unknown> = { kind: { $eq: 'company' } }
140
141
  if (query.id) filters.id = { $eq: query.id }
141
142
  if (query.search) {
@@ -24,6 +24,7 @@ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
24
24
  import { isUuidListParam, splitIdListParam } from '@open-mercato/shared/lib/crud/ids'
25
25
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
26
26
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
27
+ import { expandCreatedAtDayRules } from '../../lib/createdAtDayFilter'
27
28
  import {
28
29
  createCustomersCrudOpenApi,
29
30
  createPagedListResponseSchema,
@@ -128,7 +129,7 @@ const crud = makeCrudRoute({
128
129
  updatedAt: 'updated_at',
129
130
  },
130
131
  buildFilters: async (query, ctx) => {
131
- const advancedFilterTree = consumeAdvancedFilterState(query)
132
+ const advancedFilterTree = expandCreatedAtDayRules(consumeAdvancedFilterState(query), 'UTC')
132
133
  const filters: Record<string, unknown> = { kind: { $eq: 'person' } }
133
134
  if (query.id) filters.id = { $eq: query.id }
134
135
  if (query.search) {
@@ -58,6 +58,9 @@ import {
58
58
  } from '../../../components/detail/assignableStaff'
59
59
  import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
60
60
  import { appendCustomerListSortParams } from '../listSorting'
61
+ import { expandCreatedAtDayRules } from '../../../lib/createdAtDayFilter'
62
+ import { USER_TIMEZONE } from '../../../lib/localDay'
63
+ import { formatInTimeZone } from 'date-fns-tz'
61
64
 
62
65
  type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
63
66
 
@@ -74,8 +77,9 @@ function makeCompaniesPresets(): FilterPreset[] {
74
77
  labelKey: 'customers.companies.presets.recentlyCreated',
75
78
  iconName: 'clock',
76
79
  build: ({ now }) => {
77
- const cutoff = new Date(now.getTime() - 7 * 24 * 3600 * 1000).toISOString().slice(0, 10)
78
- return makeRuleTree({ field: 'created_at', operator: 'is_after', value: cutoff })
80
+ const dayBeforeWindow = new Date(`${formatInTimeZone(now, USER_TIMEZONE, 'yyyy-MM-dd')}T00:00:00.000Z`)
81
+ dayBeforeWindow.setUTCDate(dayBeforeWindow.getUTCDate() - 8)
82
+ return makeRuleTree({ field: 'created_at', operator: 'is_after', value: dayBeforeWindow.toISOString().slice(0, 10) })
79
83
  },
80
84
  },
81
85
  {
@@ -112,6 +116,7 @@ type CompanyRow = {
112
116
  organizationId?: string | null
113
117
  source?: string | null
114
118
  ownerUserId?: string | null
119
+ createdAt?: string | null
115
120
  } & Record<string, unknown>
116
121
 
117
122
  type CompaniesResponse = {
@@ -159,6 +164,7 @@ function mapApiItem(item: Record<string, unknown>): CompanyRow | null {
159
164
  const nextInteractionColor = typeof item.next_interaction_color === 'string' ? item.next_interaction_color : null
160
165
  const organizationId = typeof item.organization_id === 'string' ? item.organization_id : null
161
166
  const source = typeof item.source === 'string' ? item.source : null
167
+ const createdAt = typeof item.created_at === 'string' ? item.created_at : null
162
168
  const ownerUserId = typeof item.owner_user_id === 'string' ? item.owner_user_id : null
163
169
  const customFields: Record<string, unknown> = {}
164
170
  for (const [key, value] of Object.entries(item)) {
@@ -188,6 +194,7 @@ function mapApiItem(item: Record<string, unknown>): CompanyRow | null {
188
194
  organizationId,
189
195
  source,
190
196
  ownerUserId,
197
+ createdAt,
191
198
  ...customFields,
192
199
  }, item)
193
200
  }
@@ -364,7 +371,7 @@ export default function CustomersCompaniesPage() {
364
371
  params.set('pageSize', String(pageSize))
365
372
  appendCustomerListSortParams(params, sorting)
366
373
  if (search.trim()) params.set('search', search.trim())
367
- const advancedParams = serializeTree(advancedFilterState)
374
+ const advancedParams = serializeTree(expandCreatedAtDayRules(advancedFilterState, USER_TIMEZONE))
368
375
  for (const [key, val] of Object.entries(advancedParams)) {
369
376
  params.set(key, val)
370
377
  }
@@ -707,6 +714,22 @@ export default function CustomersCompaniesPage() {
707
714
  )
708
715
  : noValue,
709
716
  },
717
+ {
718
+ accessorKey: 'createdAt',
719
+ header: t('customers.companies.list.columns.createdAt', 'Created'),
720
+ meta: {
721
+ columnChooserGroup: t('customers.columnGroups.dates', 'Dates'),
722
+ filterKey: 'created_at',
723
+ filterType: 'date' as const,
724
+ filterGroup: t('customers.columnGroups.activity', 'Activity'),
725
+ filterIconName: 'calendar',
726
+ },
727
+ cell: ({ row }) => (
728
+ <span className="text-sm">
729
+ {formatDate(row.original.createdAt, t('customers.companies.list.noValue'))}
730
+ </span>
731
+ ),
732
+ },
710
733
  {
711
734
  accessorKey: 'source',
712
735
  header: t('customers.companies.list.columns.source'),
@@ -7,6 +7,7 @@ const CUSTOMER_LIST_SORT_FIELDS: Record<string, string> = {
7
7
  lifecycleStage: 'lifecycleStage',
8
8
  source: 'source',
9
9
  nextInteractionAt: 'nextInteractionAt',
10
+ createdAt: 'createdAt',
10
11
  }
11
12
 
12
13
  export function resolveCustomerListSortField(columnId: string): string | null {
@@ -58,6 +58,8 @@ import {
58
58
  } from '../../../components/detail/assignableStaff'
59
59
  import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
60
60
  import { appendCustomerListSortParams } from '../listSorting'
61
+ import { expandCreatedAtDayRules } from '../../../lib/createdAtDayFilter'
62
+ import { USER_TIMEZONE } from '../../../lib/localDay'
61
63
 
62
64
  type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
63
65
 
@@ -119,6 +121,7 @@ type PersonRow = {
119
121
  organizationId?: string | null
120
122
  source?: string | null
121
123
  ownerUserId?: string | null
124
+ createdAt?: string | null
122
125
  } & Record<string, unknown>
123
126
 
124
127
  type PeopleResponse = {
@@ -166,6 +169,7 @@ function mapApiItem(item: Record<string, unknown>): PersonRow | null {
166
169
  const nextInteractionColor = typeof item.next_interaction_color === 'string' ? item.next_interaction_color : null
167
170
  const organizationId = typeof item.organization_id === 'string' ? item.organization_id : null
168
171
  const source = typeof item.source === 'string' ? item.source : null
172
+ const createdAt = typeof item.created_at === 'string' ? item.created_at : null
169
173
  const customFields: Record<string, unknown> = {}
170
174
  for (const [key, value] of Object.entries(item)) {
171
175
  if (key.startsWith('cf_')) {
@@ -196,6 +200,7 @@ function mapApiItem(item: Record<string, unknown>): PersonRow | null {
196
200
  nextInteractionColor,
197
201
  organizationId,
198
202
  source,
203
+ createdAt,
199
204
  ...customFields,
200
205
  }, item)
201
206
  }
@@ -375,7 +380,7 @@ export default function CustomersPeoplePage() {
375
380
  params.set('pageSize', String(pageSize))
376
381
  appendCustomerListSortParams(params, sorting)
377
382
  if (search.trim()) params.set('search', search.trim())
378
- const advancedParams = serializeTree(advancedFilterState)
383
+ const advancedParams = serializeTree(expandCreatedAtDayRules(advancedFilterState, USER_TIMEZONE))
379
384
  for (const [key, val] of Object.entries(advancedParams)) {
380
385
  params.set(key, val)
381
386
  }
@@ -713,6 +718,22 @@ export default function CustomersPeoplePage() {
713
718
  )
714
719
  : <span className="text-muted-foreground text-sm">{t('customers.people.list.noValue')}</span>,
715
720
  },
721
+ {
722
+ accessorKey: 'createdAt',
723
+ header: t('customers.people.list.columns.createdAt', 'Created'),
724
+ meta: {
725
+ columnChooserGroup: t('customers.columnGroups.dates', 'Dates'),
726
+ filterKey: 'created_at',
727
+ filterType: 'date' as const,
728
+ filterGroup: t('customers.columnGroups.activity', 'Activity'),
729
+ filterIconName: 'calendar',
730
+ },
731
+ cell: ({ row }) => (
732
+ <span className="text-sm">
733
+ {formatDate(row.original.createdAt, t('customers.people.list.noValue'))}
734
+ </span>
735
+ ),
736
+ },
716
737
  {
717
738
  accessorKey: 'source',
718
739
  header: t('customers.people.list.columns.source'),
@@ -797,6 +797,7 @@
797
797
  "customers.companies.list.bulkDelete.progressName": "Ausgewählte Unternehmen löschen",
798
798
  "customers.companies.list.bulkDelete.success": "{count} companies deleted",
799
799
  "customers.companies.list.bulkDelete.title": "Delete {count} companies?",
800
+ "customers.companies.list.columns.createdAt": "Erstellt am",
800
801
  "customers.companies.list.columns.email": "E-Mail",
801
802
  "customers.companies.list.columns.lifecycleStage": "Lebenszyklusphase",
802
803
  "customers.companies.list.columns.name": "Name",
@@ -2512,6 +2513,7 @@
2512
2513
  "customers.people.list.bulkDelete.progressName": "Ausgewählte Personen löschen",
2513
2514
  "customers.people.list.bulkDelete.success": "{count} people deleted",
2514
2515
  "customers.people.list.bulkDelete.title": "Delete {count} people?",
2516
+ "customers.people.list.columns.createdAt": "Erstellt am",
2515
2517
  "customers.people.list.columns.email": "E-Mail",
2516
2518
  "customers.people.list.columns.lifecycleStage": "Lebenszyklusphase",
2517
2519
  "customers.people.list.columns.name": "Name",
@@ -797,6 +797,7 @@
797
797
  "customers.companies.list.bulkDelete.progressName": "Delete selected companies",
798
798
  "customers.companies.list.bulkDelete.success": "{count} companies deleted",
799
799
  "customers.companies.list.bulkDelete.title": "Delete {count} companies?",
800
+ "customers.companies.list.columns.createdAt": "Created",
800
801
  "customers.companies.list.columns.email": "Email",
801
802
  "customers.companies.list.columns.lifecycleStage": "Lifecycle stage",
802
803
  "customers.companies.list.columns.name": "Name",
@@ -2512,6 +2513,7 @@
2512
2513
  "customers.people.list.bulkDelete.progressName": "Delete selected people",
2513
2514
  "customers.people.list.bulkDelete.success": "{count} people deleted",
2514
2515
  "customers.people.list.bulkDelete.title": "Delete {count} people?",
2516
+ "customers.people.list.columns.createdAt": "Created",
2515
2517
  "customers.people.list.columns.email": "Email",
2516
2518
  "customers.people.list.columns.lifecycleStage": "Lifecycle stage",
2517
2519
  "customers.people.list.columns.name": "Name",
@@ -797,6 +797,7 @@
797
797
  "customers.companies.list.bulkDelete.progressName": "Eliminar empresas seleccionadas",
798
798
  "customers.companies.list.bulkDelete.success": "{count} companies deleted",
799
799
  "customers.companies.list.bulkDelete.title": "Delete {count} companies?",
800
+ "customers.companies.list.columns.createdAt": "Fecha de creación",
800
801
  "customers.companies.list.columns.email": "Correo electrónico",
801
802
  "customers.companies.list.columns.lifecycleStage": "Etapa del ciclo de vida",
802
803
  "customers.companies.list.columns.name": "Nombre",
@@ -2512,6 +2513,7 @@
2512
2513
  "customers.people.list.bulkDelete.progressName": "Eliminar personas seleccionadas",
2513
2514
  "customers.people.list.bulkDelete.success": "{count} people deleted",
2514
2515
  "customers.people.list.bulkDelete.title": "Delete {count} people?",
2516
+ "customers.people.list.columns.createdAt": "Fecha de creación",
2515
2517
  "customers.people.list.columns.email": "Correo electrónico",
2516
2518
  "customers.people.list.columns.lifecycleStage": "Etapa del ciclo de vida",
2517
2519
  "customers.people.list.columns.name": "Nombre",
@@ -797,6 +797,7 @@
797
797
  "customers.companies.list.bulkDelete.progressName": "선택한 회사 삭제",
798
798
  "customers.companies.list.bulkDelete.success": "회사 {count}개가 삭제되었습니다",
799
799
  "customers.companies.list.bulkDelete.title": "회사 {count}개를 삭제하시겠습니까?",
800
+ "customers.companies.list.columns.createdAt": "생성일",
800
801
  "customers.companies.list.columns.email": "이메일",
801
802
  "customers.companies.list.columns.lifecycleStage": "라이프사이클 단계",
802
803
  "customers.companies.list.columns.name": "이름",
@@ -2512,6 +2513,7 @@
2512
2513
  "customers.people.list.bulkDelete.progressName": "선택한 연락처 삭제",
2513
2514
  "customers.people.list.bulkDelete.success": "{count}명의 연락처가 삭제되었습니다",
2514
2515
  "customers.people.list.bulkDelete.title": "{count}명의 연락처를 삭제하시겠습니까?",
2516
+ "customers.people.list.columns.createdAt": "생성일",
2515
2517
  "customers.people.list.columns.email": "이메일",
2516
2518
  "customers.people.list.columns.lifecycleStage": "라이프사이클 단계",
2517
2519
  "customers.people.list.columns.name": "이름",
@@ -797,6 +797,7 @@
797
797
  "customers.companies.list.bulkDelete.progressName": "Usuń zaznaczone firmy",
798
798
  "customers.companies.list.bulkDelete.success": "Usunięto {count} firm",
799
799
  "customers.companies.list.bulkDelete.title": "Usunąć {count} firm?",
800
+ "customers.companies.list.columns.createdAt": "Data utworzenia",
800
801
  "customers.companies.list.columns.email": "E-mail",
801
802
  "customers.companies.list.columns.lifecycleStage": "Etap cyklu życia",
802
803
  "customers.companies.list.columns.name": "Nazwa",
@@ -2512,6 +2513,7 @@
2512
2513
  "customers.people.list.bulkDelete.progressName": "Usuń zaznaczone osoby",
2513
2514
  "customers.people.list.bulkDelete.success": "Usunięto {count} osób",
2514
2515
  "customers.people.list.bulkDelete.title": "Usunąć {count} osób?",
2516
+ "customers.people.list.columns.createdAt": "Data utworzenia",
2515
2517
  "customers.people.list.columns.email": "E-mail",
2516
2518
  "customers.people.list.columns.lifecycleStage": "Etap cyklu życia",
2517
2519
  "customers.people.list.columns.name": "Nazwa",