@open-mercato/core 0.6.8-develop.7004.1.7e7dd67d56 → 0.6.8-develop.7008.1.1ab31c0ca9

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.
@@ -5,6 +5,7 @@ import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimi
5
5
  import { resolveTodoApiPath } from "../utils.js";
6
6
  import { generateTempId } from "@open-mercato/core/modules/customers/lib/detailHelpers";
7
7
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
8
+ import { hasMoreFromPage } from "@open-mercato/shared/lib/pagination/load-more";
8
9
  import { CUSTOMER_INTERACTION_TASK_SOURCE } from "../../../lib/interactionCompatibility.js";
9
10
  const DEFAULT_TODO_SOURCE = CUSTOMER_INTERACTION_TASK_SOURCE;
10
11
  function mapRowToSummary(row) {
@@ -97,7 +98,7 @@ function usePersonTasks({
97
98
  const [tasks, setTasks] = React.useState(initialTasks);
98
99
  const [pageInfo, setPageInfo] = React.useState({
99
100
  page: 1,
100
- totalPages: 1,
101
+ hasMore: false,
101
102
  total: initialTasks.length
102
103
  });
103
104
  const [isInitialLoading, setIsInitialLoading] = React.useState(() => Boolean(entityId));
@@ -109,12 +110,18 @@ function usePersonTasks({
109
110
  const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : [];
110
111
  setPageInfo({
111
112
  page: payload.page ?? 1,
112
- totalPages: payload.totalPages ?? 0,
113
+ // Short-page termination instead of `page < totalPages` see
114
+ // `hasMoreFromPage`. `mapRowToSummary` is a 1:1 map and the `mergeUnique`
115
+ // dedupe happens after this, so `mapped.length` is what the server served.
116
+ // Measured against the page size the server echoed rather than the one
117
+ // requested, so a page size the endpoint narrows server-side cannot make a
118
+ // full page read as short and silently end the sequence.
119
+ hasMore: hasMoreFromPage(mapped.length, payload.pageSize ?? pageSize),
113
120
  total: payload.total ?? mapped.length
114
121
  });
115
122
  setError(null);
116
123
  return mapped;
117
- }, []);
124
+ }, [pageSize]);
118
125
  const fetchPage = React.useCallback(
119
126
  async (page) => {
120
127
  if (!entityId) {
@@ -122,8 +129,7 @@ function usePersonTasks({
122
129
  items: [],
123
130
  total: 0,
124
131
  page: 1,
125
- pageSize,
126
- totalPages: 1
132
+ pageSize
127
133
  };
128
134
  }
129
135
  const params = new URLSearchParams({
@@ -142,7 +148,7 @@ function usePersonTasks({
142
148
  const refresh = React.useCallback(async () => {
143
149
  if (!entityId) {
144
150
  setTasks([]);
145
- setPageInfo({ page: 1, totalPages: 1, total: 0 });
151
+ setPageInfo({ page: 1, hasMore: false, total: 0 });
146
152
  return;
147
153
  }
148
154
  setIsInitialLoading(true);
@@ -161,7 +167,7 @@ function usePersonTasks({
161
167
  const loadMore = React.useCallback(async () => {
162
168
  if (!entityId) return;
163
169
  if (isLoadingMore) return;
164
- if (pageInfo.page >= pageInfo.totalPages) return;
170
+ if (!pageInfo.hasMore) return;
165
171
  setIsLoadingMore(true);
166
172
  try {
167
173
  const payload = await fetchPage(pageInfo.page + 1);
@@ -174,11 +180,11 @@ function usePersonTasks({
174
180
  } finally {
175
181
  setIsLoadingMore(false);
176
182
  }
177
- }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.page, pageInfo.totalPages]);
183
+ }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.hasMore, pageInfo.page]);
178
184
  React.useEffect(() => {
179
185
  if (!entityId) {
180
186
  setTasks([]);
181
- setPageInfo({ page: 1, totalPages: 1, total: 0 });
187
+ setPageInfo({ page: 1, hasMore: false, total: 0 });
182
188
  setError(null);
183
189
  setIsInitialLoading(false);
184
190
  return;
@@ -186,7 +192,7 @@ function usePersonTasks({
186
192
  setTasks(initialTasks);
187
193
  setPageInfo({
188
194
  page: 1,
189
- totalPages: 1,
195
+ hasMore: false,
190
196
  total: initialTasks.length
191
197
  });
192
198
  setError(null);
@@ -255,7 +261,7 @@ function usePersonTasks({
255
261
  setTasks((prev) => [newTask, ...prev]);
256
262
  setPageInfo((prev) => ({
257
263
  page: 1,
258
- totalPages: prev.totalPages,
264
+ hasMore: prev.hasMore,
259
265
  total: prev.total + 1
260
266
  }));
261
267
  await refresh();
@@ -361,7 +367,7 @@ function usePersonTasks({
361
367
  setTasks((prev) => prev.filter((item) => item.id !== task.id));
362
368
  setPageInfo((prev) => ({
363
369
  page: prev.page,
364
- totalPages: prev.totalPages,
370
+ hasMore: prev.hasMore,
365
371
  total: Math.max(0, prev.total - 1)
366
372
  }));
367
373
  } finally {
@@ -370,7 +376,7 @@ function usePersonTasks({
370
376
  },
371
377
  []
372
378
  );
373
- const hasMore = entityId != null && pageInfo.page < pageInfo.totalPages;
379
+ const hasMore = entityId != null && pageInfo.hasMore;
374
380
  return {
375
381
  tasks,
376
382
  isInitialLoading,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/components/detail/hooks/usePersonTasks.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { resolveTodoApiPath } from '../utils'\nimport type { TodoLinkSummary } from '../types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE } from '../../../lib/interactionCompatibility'\n\nconst DEFAULT_TODO_SOURCE = CUSTOMER_INTERACTION_TASK_SOURCE\n\ntype CustomerTodoRow = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n todoIsDone: boolean | null\n todoPriority: number | null\n todoSeverity: string | null\n todoDescription: string | null\n todoDueAt: string | null\n todoCustomValues: Record<string, unknown> | null\n todoOrganizationId: string | null\n todoUpdatedAt?: string | null\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\ntype CustomerTodosResponse = {\n items: CustomerTodoRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nexport type TaskFormPayload = {\n base: {\n title: string\n is_done?: boolean\n status?: string\n description?: string | null\n priority?: number | null\n scheduledAt?: string | null\n }\n custom: Record<string, unknown>\n}\n\nexport type UsePersonTasksOptions = {\n entityId: string | null\n initialTasks?: TodoLinkSummary[]\n pageSize?: number\n}\n\nexport type UsePersonTasksResult = {\n tasks: TodoLinkSummary[]\n isInitialLoading: boolean\n isLoadingMore: boolean\n isMutating: boolean\n hasMore: boolean\n loadMore: () => Promise<void>\n refresh: () => Promise<void>\n createTask: (payload: TaskFormPayload) => Promise<void>\n updateTask: (task: TodoLinkSummary, payload: TaskFormPayload) => Promise<void>\n toggleTask: (task: TodoLinkSummary, nextIsDone: boolean) => Promise<void>\n unlinkTask: (task: TodoLinkSummary) => Promise<void>\n pendingTaskId: string | null\n totalCount: number\n error: string | null\n}\n\nfunction mapRowToSummary(row: CustomerTodoRow): TodoLinkSummary {\n return {\n id: row.id,\n todoId: row.todoId,\n todoSource: row.todoSource || DEFAULT_TODO_SOURCE,\n createdAt: row.createdAt,\n title: row.todoTitle ?? null,\n isDone: row.todoIsDone ?? null,\n status: row.todoIsDone ? 'done' : 'planned',\n priority: row.todoPriority ?? null,\n severity: row.todoSeverity ?? null,\n description: row.todoDescription ?? null,\n dueAt: row.todoDueAt ?? null,\n todoOrganizationId: row.todoOrganizationId ?? null,\n updatedAt: row.todoUpdatedAt ?? null,\n customValues: row.todoCustomValues ?? null,\n }\n}\n\nfunction mergeUnique(existing: TodoLinkSummary[], incoming: TodoLinkSummary[]): TodoLinkSummary[] {\n if (!existing.length) return incoming\n if (!incoming.length) return existing\n const byId = new Map<string, TodoLinkSummary>()\n const result: TodoLinkSummary[] = []\n for (const item of existing) {\n byId.set(item.id, item)\n result.push(item)\n }\n for (const item of incoming) {\n if (byId.has(item.id)) {\n const index = result.findIndex((entry) => entry.id === item.id)\n if (index !== -1) result[index] = item\n } else {\n byId.set(item.id, item)\n result.push(item)\n }\n }\n return result\n}\n\nfunction normalizeBoolean(value: unknown): boolean | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const parsed = parseBooleanToken(value)\n return parsed === null ? undefined : parsed\n }\n return undefined\n}\n\nfunction normalizeNumber(value: unknown): number | null | undefined {\n if (value === null || value === undefined || value === '') return undefined\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n const parsed = Number(trimmed)\n if (!Number.isNaN(parsed)) return parsed\n }\n return null\n}\n\nfunction normalizeString(value: unknown): string | null | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n }\n return String(value)\n}\n\nfunction buildLegacyTaskCustomValues(\n payload: TaskFormPayload,\n options?: { includeEmptyFields?: boolean },\n): Record<string, unknown> {\n const includeEmptyFields = options?.includeEmptyFields === true\n const custom = { ...payload.custom }\n\n const assignValue = (key: string, value: unknown) => {\n if (value === undefined) return\n if (value === null) {\n if (includeEmptyFields) custom[key] = null\n return\n }\n custom[key] = value\n }\n\n assignValue('priority', payload.base.priority ?? null)\n assignValue('description', payload.base.description ?? null)\n assignValue('due_at', payload.base.scheduledAt ?? null)\n\n return custom\n}\n\nexport function usePersonTasks({\n entityId,\n initialTasks = [],\n pageSize = 20,\n}: UsePersonTasksOptions): UsePersonTasksResult {\n const [tasks, setTasks] = React.useState<TodoLinkSummary[]>(initialTasks)\n const [pageInfo, setPageInfo] = React.useState<{ page: number; totalPages: number; total: number }>({\n page: 1,\n totalPages: 1,\n total: initialTasks.length,\n })\n const [isInitialLoading, setIsInitialLoading] = React.useState<boolean>(() => Boolean(entityId))\n const [isLoadingMore, setIsLoadingMore] = React.useState(false)\n const [isMutating, setIsMutating] = React.useState(false)\n const [pendingTaskId, setPendingTaskId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const mapResponse = React.useCallback((payload: CustomerTodosResponse) => {\n const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []\n setPageInfo({\n page: payload.page ?? 1,\n totalPages: payload.totalPages ?? 0,\n total: payload.total ?? mapped.length,\n })\n setError(null)\n return mapped\n }, [])\n\n const fetchPage = React.useCallback(\n async (page: number): Promise<CustomerTodosResponse> => {\n if (!entityId) {\n return {\n items: [],\n total: 0,\n page: 1,\n pageSize,\n totalPages: 1,\n }\n }\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n entityId,\n })\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to load tasks.' },\n )\n },\n [entityId, pageSize],\n )\n\n const refresh = React.useCallback(async () => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, totalPages: 1, total: 0 })\n return\n }\n setIsInitialLoading(true)\n try {\n const payload = await fetchPage(1)\n const mapped = mapResponse(payload)\n setTasks(mapped)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsInitialLoading(false)\n }\n }, [entityId, fetchPage, mapResponse])\n\n const loadMore = React.useCallback(async () => {\n if (!entityId) return\n if (isLoadingMore) return\n if (pageInfo.page >= pageInfo.totalPages) return\n setIsLoadingMore(true)\n try {\n const payload = await fetchPage(pageInfo.page + 1)\n const mapped = mapResponse(payload)\n setTasks((prev) => mergeUnique(prev, mapped))\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsLoadingMore(false)\n }\n }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.page, pageInfo.totalPages])\n\n React.useEffect(() => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, totalPages: 1, total: 0 })\n setError(null)\n setIsInitialLoading(false)\n return\n }\n setTasks(initialTasks)\n setPageInfo({\n page: 1,\n totalPages: 1,\n total: initialTasks.length,\n })\n setError(null)\n let cancelled = false\n setIsInitialLoading(true)\n fetchPage(1)\n .then((payload) => {\n if (cancelled) return\n const mapped = mapResponse(payload)\n setTasks(mapped)\n })\n .catch((err) => {\n if (cancelled) return\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n })\n .finally(() => {\n if (!cancelled) setIsInitialLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [entityId, initialTasks, fetchPage, mapResponse])\n\n const createTask = React.useCallback(\n async ({ base, custom }: TaskFormPayload) => {\n if (!entityId) throw new Error('Task creation requires an entity id')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom })\n const payload: Record<string, unknown> = {\n entityId,\n title: base.title,\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) payload.isDone = normalizedDone\n if (Object.keys(customValues).length) payload.todoCustom = customValues\n\n const response = await apiCallOrThrow<{ linkId?: string; todoId?: string }>(\n '/api/customers/todos',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: 'Failed to create task.' },\n )\n const body = response.result ?? {}\n const linkId = typeof body.linkId === 'string' && body.linkId.length ? body.linkId : generateTempId()\n const todoId = typeof body.todoId === 'string' && body.todoId.length ? body.todoId : generateTempId()\n const createdAt = new Date().toISOString()\n const persistedCustomValues = Object.keys(customValues).length ? { ...customValues } : null\n const priority = normalizeNumber(base.priority ?? customValues.priority)\n const severity = normalizeString(customValues.severity) ?? null\n const description = normalizeString(base.description ?? customValues.description) ?? null\n const newTask: TodoLinkSummary = {\n id: linkId,\n todoId,\n todoSource: DEFAULT_TODO_SOURCE,\n createdAt,\n title: base.title,\n isDone: normalizedDone ?? false,\n status: normalizedDone ? 'done' : 'planned',\n priority: priority === undefined ? null : priority,\n severity,\n description,\n dueAt: normalizeString(base.scheduledAt ?? customValues.due_at) ?? normalizeString(customValues.dueAt) ?? null,\n todoOrganizationId: null,\n customValues: persistedCustomValues,\n }\n setTasks((prev) => [newTask, ...prev])\n setPageInfo((prev) => ({\n page: 1,\n totalPages: prev.totalPages,\n total: prev.total + 1,\n }))\n await refresh()\n } finally {\n setIsMutating(false)\n }\n },\n [entityId, refresh],\n )\n\n const updateTask = React.useCallback(\n async (task: TodoLinkSummary, { base, custom }: TaskFormPayload) => {\n if (!task.todoId) throw new Error('Task is missing todo id')\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) throw new Error('Unsupported task source')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom }, { includeEmptyFields: true })\n const body: Record<string, unknown> = {\n id: task.todoId,\n linkId: task.id,\n }\n if (typeof base.title === 'string' && base.title.trim().length) {\n body.title = base.title.trim()\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) body.is_done = normalizedDone\n if (Object.keys(customValues).length) {\n body.customFields = customValues\n }\n // Send the optimistic-lock header (task's loaded updatedAt) so a stale\n // edit \u2014 or an edit after the task was deleted in another tab \u2014 surfaces\n // the unified conflict bar (409) instead of silently overwriting or\n // returning a bare \"Interaction not found\" 404 (#2055).\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n apiPath,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: 'Failed to update task.' },\n ),\n )\n setTasks((prev) =>\n prev.map((item) => {\n if (item.id !== task.id) return item\n const nextCustomValues = { ...(item.customValues ?? {}) }\n for (const [key, value] of Object.entries(customValues)) {\n nextCustomValues[key] = value === undefined ? null : value\n }\n return {\n ...item,\n title: typeof base.title === 'string' && base.title.trim().length ? base.title.trim() : item.title,\n isDone: normalizedDone !== undefined ? normalizedDone : item.isDone,\n status: normalizedDone !== undefined ? (normalizedDone ? 'done' : 'planned') : item.status ?? null,\n priority:\n normalizeNumber(base.priority ?? customValues.priority) ??\n (base.priority === undefined && customValues.priority === undefined ? item.priority ?? null : null),\n severity:\n normalizeString(customValues.severity) ??\n (customValues.severity === undefined ? item.severity ?? null : null),\n description:\n normalizeString(base.description ?? customValues.description) ??\n (base.description === undefined && customValues.description === undefined ? item.description ?? null : null),\n dueAt:\n normalizeString(base.scheduledAt ?? customValues.due_at) ??\n normalizeString(customValues.dueAt) ??\n (base.scheduledAt === undefined && customValues.due_at === undefined && customValues.dueAt === undefined\n ? item.dueAt ?? null\n : null),\n customValues: Object.keys(nextCustomValues).length ? nextCustomValues : null,\n }\n }),\n )\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const toggleTask = React.useCallback(\n async (task: TodoLinkSummary, nextIsDone: boolean) => {\n if (!task.todoId) {\n throw new Error('Task is missing todo id')\n }\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) {\n throw new Error('Unsupported task source')\n }\n setPendingTaskId(task.todoId)\n try {\n await updateTask(task, { base: { title: task.title ?? '', is_done: nextIsDone }, custom: {} })\n } finally {\n setPendingTaskId(null)\n }\n },\n [updateTask],\n )\n\n const unlinkTask = React.useCallback(\n async (task: TodoLinkSummary) => {\n if (!task.id) throw new Error('Task link id missing')\n setIsMutating(true)\n try {\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n '/api/customers/todos',\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: task.id }),\n },\n { errorMessage: 'Failed to remove task.' },\n ),\n )\n setTasks((prev) => prev.filter((item) => item.id !== task.id))\n setPageInfo((prev) => ({\n page: prev.page,\n totalPages: prev.totalPages,\n total: Math.max(0, prev.total - 1),\n }))\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const hasMore = entityId != null && pageInfo.page < pageInfo.totalPages\n\n return {\n tasks,\n isInitialLoading,\n isLoadingMore,\n isMutating,\n hasMore,\n loadMore,\n refresh,\n createTask,\n updateTask,\n toggleTask,\n unlinkTask,\n pendingTaskId,\n totalCount: pageInfo.total,\n error,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { resolveTodoApiPath } from '../utils'\nimport type { TodoLinkSummary } from '../types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE } from '../../../lib/interactionCompatibility'\n\nconst DEFAULT_TODO_SOURCE = CUSTOMER_INTERACTION_TASK_SOURCE\n\ntype CustomerTodoRow = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n todoIsDone: boolean | null\n todoPriority: number | null\n todoSeverity: string | null\n todoDescription: string | null\n todoDueAt: string | null\n todoCustomValues: Record<string, unknown> | null\n todoOrganizationId: string | null\n todoUpdatedAt?: string | null\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\n// `/api/customers/todos` also reports `totalPages`, deliberately left out of\n// this type: the load-more affordance terminates on a short page instead \u2014 see\n// `hasMoreFromPage`. `total` stays because it feeds the section's count badge,\n// where an under-report is cosmetic rather than a way to strand rows.\ntype CustomerTodosResponse = {\n items: CustomerTodoRow[]\n total: number\n page: number\n pageSize: number\n}\n\nexport type TaskFormPayload = {\n base: {\n title: string\n is_done?: boolean\n status?: string\n description?: string | null\n priority?: number | null\n scheduledAt?: string | null\n }\n custom: Record<string, unknown>\n}\n\nexport type UsePersonTasksOptions = {\n entityId: string | null\n initialTasks?: TodoLinkSummary[]\n pageSize?: number\n}\n\nexport type UsePersonTasksResult = {\n tasks: TodoLinkSummary[]\n isInitialLoading: boolean\n isLoadingMore: boolean\n isMutating: boolean\n hasMore: boolean\n loadMore: () => Promise<void>\n refresh: () => Promise<void>\n createTask: (payload: TaskFormPayload) => Promise<void>\n updateTask: (task: TodoLinkSummary, payload: TaskFormPayload) => Promise<void>\n toggleTask: (task: TodoLinkSummary, nextIsDone: boolean) => Promise<void>\n unlinkTask: (task: TodoLinkSummary) => Promise<void>\n pendingTaskId: string | null\n totalCount: number\n error: string | null\n}\n\nfunction mapRowToSummary(row: CustomerTodoRow): TodoLinkSummary {\n return {\n id: row.id,\n todoId: row.todoId,\n todoSource: row.todoSource || DEFAULT_TODO_SOURCE,\n createdAt: row.createdAt,\n title: row.todoTitle ?? null,\n isDone: row.todoIsDone ?? null,\n status: row.todoIsDone ? 'done' : 'planned',\n priority: row.todoPriority ?? null,\n severity: row.todoSeverity ?? null,\n description: row.todoDescription ?? null,\n dueAt: row.todoDueAt ?? null,\n todoOrganizationId: row.todoOrganizationId ?? null,\n updatedAt: row.todoUpdatedAt ?? null,\n customValues: row.todoCustomValues ?? null,\n }\n}\n\nfunction mergeUnique(existing: TodoLinkSummary[], incoming: TodoLinkSummary[]): TodoLinkSummary[] {\n if (!existing.length) return incoming\n if (!incoming.length) return existing\n const byId = new Map<string, TodoLinkSummary>()\n const result: TodoLinkSummary[] = []\n for (const item of existing) {\n byId.set(item.id, item)\n result.push(item)\n }\n for (const item of incoming) {\n if (byId.has(item.id)) {\n const index = result.findIndex((entry) => entry.id === item.id)\n if (index !== -1) result[index] = item\n } else {\n byId.set(item.id, item)\n result.push(item)\n }\n }\n return result\n}\n\nfunction normalizeBoolean(value: unknown): boolean | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const parsed = parseBooleanToken(value)\n return parsed === null ? undefined : parsed\n }\n return undefined\n}\n\nfunction normalizeNumber(value: unknown): number | null | undefined {\n if (value === null || value === undefined || value === '') return undefined\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n const parsed = Number(trimmed)\n if (!Number.isNaN(parsed)) return parsed\n }\n return null\n}\n\nfunction normalizeString(value: unknown): string | null | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n }\n return String(value)\n}\n\nfunction buildLegacyTaskCustomValues(\n payload: TaskFormPayload,\n options?: { includeEmptyFields?: boolean },\n): Record<string, unknown> {\n const includeEmptyFields = options?.includeEmptyFields === true\n const custom = { ...payload.custom }\n\n const assignValue = (key: string, value: unknown) => {\n if (value === undefined) return\n if (value === null) {\n if (includeEmptyFields) custom[key] = null\n return\n }\n custom[key] = value\n }\n\n assignValue('priority', payload.base.priority ?? null)\n assignValue('description', payload.base.description ?? null)\n assignValue('due_at', payload.base.scheduledAt ?? null)\n\n return custom\n}\n\nexport function usePersonTasks({\n entityId,\n initialTasks = [],\n pageSize = 20,\n}: UsePersonTasksOptions): UsePersonTasksResult {\n const [tasks, setTasks] = React.useState<TodoLinkSummary[]>(initialTasks)\n const [pageInfo, setPageInfo] = React.useState<{ page: number; hasMore: boolean; total: number }>({\n page: 1,\n hasMore: false,\n total: initialTasks.length,\n })\n const [isInitialLoading, setIsInitialLoading] = React.useState<boolean>(() => Boolean(entityId))\n const [isLoadingMore, setIsLoadingMore] = React.useState(false)\n const [isMutating, setIsMutating] = React.useState(false)\n const [pendingTaskId, setPendingTaskId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const mapResponse = React.useCallback((payload: CustomerTodosResponse) => {\n const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []\n setPageInfo({\n page: payload.page ?? 1,\n // Short-page termination instead of `page < totalPages` \u2014 see\n // `hasMoreFromPage`. `mapRowToSummary` is a 1:1 map and the `mergeUnique`\n // dedupe happens after this, so `mapped.length` is what the server served.\n // Measured against the page size the server echoed rather than the one\n // requested, so a page size the endpoint narrows server-side cannot make a\n // full page read as short and silently end the sequence.\n hasMore: hasMoreFromPage(mapped.length, payload.pageSize ?? pageSize),\n total: payload.total ?? mapped.length,\n })\n setError(null)\n return mapped\n }, [pageSize])\n\n const fetchPage = React.useCallback(\n async (page: number): Promise<CustomerTodosResponse> => {\n if (!entityId) {\n return {\n items: [],\n total: 0,\n page: 1,\n pageSize,\n }\n }\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n entityId,\n })\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to load tasks.' },\n )\n },\n [entityId, pageSize],\n )\n\n const refresh = React.useCallback(async () => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, hasMore: false, total: 0 })\n return\n }\n setIsInitialLoading(true)\n try {\n const payload = await fetchPage(1)\n const mapped = mapResponse(payload)\n setTasks(mapped)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsInitialLoading(false)\n }\n }, [entityId, fetchPage, mapResponse])\n\n const loadMore = React.useCallback(async () => {\n if (!entityId) return\n if (isLoadingMore) return\n if (!pageInfo.hasMore) return\n setIsLoadingMore(true)\n try {\n const payload = await fetchPage(pageInfo.page + 1)\n const mapped = mapResponse(payload)\n setTasks((prev) => mergeUnique(prev, mapped))\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsLoadingMore(false)\n }\n }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.hasMore, pageInfo.page])\n\n React.useEffect(() => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, hasMore: false, total: 0 })\n setError(null)\n setIsInitialLoading(false)\n return\n }\n setTasks(initialTasks)\n setPageInfo({\n page: 1,\n hasMore: false,\n total: initialTasks.length,\n })\n setError(null)\n let cancelled = false\n setIsInitialLoading(true)\n fetchPage(1)\n .then((payload) => {\n if (cancelled) return\n const mapped = mapResponse(payload)\n setTasks(mapped)\n })\n .catch((err) => {\n if (cancelled) return\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n })\n .finally(() => {\n if (!cancelled) setIsInitialLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [entityId, initialTasks, fetchPage, mapResponse])\n\n const createTask = React.useCallback(\n async ({ base, custom }: TaskFormPayload) => {\n if (!entityId) throw new Error('Task creation requires an entity id')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom })\n const payload: Record<string, unknown> = {\n entityId,\n title: base.title,\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) payload.isDone = normalizedDone\n if (Object.keys(customValues).length) payload.todoCustom = customValues\n\n const response = await apiCallOrThrow<{ linkId?: string; todoId?: string }>(\n '/api/customers/todos',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: 'Failed to create task.' },\n )\n const body = response.result ?? {}\n const linkId = typeof body.linkId === 'string' && body.linkId.length ? body.linkId : generateTempId()\n const todoId = typeof body.todoId === 'string' && body.todoId.length ? body.todoId : generateTempId()\n const createdAt = new Date().toISOString()\n const persistedCustomValues = Object.keys(customValues).length ? { ...customValues } : null\n const priority = normalizeNumber(base.priority ?? customValues.priority)\n const severity = normalizeString(customValues.severity) ?? null\n const description = normalizeString(base.description ?? customValues.description) ?? null\n const newTask: TodoLinkSummary = {\n id: linkId,\n todoId,\n todoSource: DEFAULT_TODO_SOURCE,\n createdAt,\n title: base.title,\n isDone: normalizedDone ?? false,\n status: normalizedDone ? 'done' : 'planned',\n priority: priority === undefined ? null : priority,\n severity,\n description,\n dueAt: normalizeString(base.scheduledAt ?? customValues.due_at) ?? normalizeString(customValues.dueAt) ?? null,\n todoOrganizationId: null,\n customValues: persistedCustomValues,\n }\n setTasks((prev) => [newTask, ...prev])\n setPageInfo((prev) => ({\n page: 1,\n hasMore: prev.hasMore,\n total: prev.total + 1,\n }))\n await refresh()\n } finally {\n setIsMutating(false)\n }\n },\n [entityId, refresh],\n )\n\n const updateTask = React.useCallback(\n async (task: TodoLinkSummary, { base, custom }: TaskFormPayload) => {\n if (!task.todoId) throw new Error('Task is missing todo id')\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) throw new Error('Unsupported task source')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom }, { includeEmptyFields: true })\n const body: Record<string, unknown> = {\n id: task.todoId,\n linkId: task.id,\n }\n if (typeof base.title === 'string' && base.title.trim().length) {\n body.title = base.title.trim()\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) body.is_done = normalizedDone\n if (Object.keys(customValues).length) {\n body.customFields = customValues\n }\n // Send the optimistic-lock header (task's loaded updatedAt) so a stale\n // edit \u2014 or an edit after the task was deleted in another tab \u2014 surfaces\n // the unified conflict bar (409) instead of silently overwriting or\n // returning a bare \"Interaction not found\" 404 (#2055).\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n apiPath,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: 'Failed to update task.' },\n ),\n )\n setTasks((prev) =>\n prev.map((item) => {\n if (item.id !== task.id) return item\n const nextCustomValues = { ...(item.customValues ?? {}) }\n for (const [key, value] of Object.entries(customValues)) {\n nextCustomValues[key] = value === undefined ? null : value\n }\n return {\n ...item,\n title: typeof base.title === 'string' && base.title.trim().length ? base.title.trim() : item.title,\n isDone: normalizedDone !== undefined ? normalizedDone : item.isDone,\n status: normalizedDone !== undefined ? (normalizedDone ? 'done' : 'planned') : item.status ?? null,\n priority:\n normalizeNumber(base.priority ?? customValues.priority) ??\n (base.priority === undefined && customValues.priority === undefined ? item.priority ?? null : null),\n severity:\n normalizeString(customValues.severity) ??\n (customValues.severity === undefined ? item.severity ?? null : null),\n description:\n normalizeString(base.description ?? customValues.description) ??\n (base.description === undefined && customValues.description === undefined ? item.description ?? null : null),\n dueAt:\n normalizeString(base.scheduledAt ?? customValues.due_at) ??\n normalizeString(customValues.dueAt) ??\n (base.scheduledAt === undefined && customValues.due_at === undefined && customValues.dueAt === undefined\n ? item.dueAt ?? null\n : null),\n customValues: Object.keys(nextCustomValues).length ? nextCustomValues : null,\n }\n }),\n )\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const toggleTask = React.useCallback(\n async (task: TodoLinkSummary, nextIsDone: boolean) => {\n if (!task.todoId) {\n throw new Error('Task is missing todo id')\n }\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) {\n throw new Error('Unsupported task source')\n }\n setPendingTaskId(task.todoId)\n try {\n await updateTask(task, { base: { title: task.title ?? '', is_done: nextIsDone }, custom: {} })\n } finally {\n setPendingTaskId(null)\n }\n },\n [updateTask],\n )\n\n const unlinkTask = React.useCallback(\n async (task: TodoLinkSummary) => {\n if (!task.id) throw new Error('Task link id missing')\n setIsMutating(true)\n try {\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n '/api/customers/todos',\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: task.id }),\n },\n { errorMessage: 'Failed to remove task.' },\n ),\n )\n setTasks((prev) => prev.filter((item) => item.id !== task.id))\n setPageInfo((prev) => ({\n page: prev.page,\n hasMore: prev.hasMore,\n total: Math.max(0, prev.total - 1),\n }))\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const hasMore = entityId != null && pageInfo.hasMore\n\n return {\n tasks,\n isInitialLoading,\n isLoadingMore,\n isMutating,\n hasMore,\n loadMore,\n refresh,\n createTask,\n updateTask,\n toggleTask,\n unlinkTask,\n pendingTaskId,\n totalCount: pageInfo.total,\n error,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,8 @@ import {
13
13
  resolveJoins
14
14
  } from "@open-mercato/shared/lib/query/join-utils";
15
15
  import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
16
+ import { isEncryptedLikeField, resolveEncryptedLikeFieldSet } from "@open-mercato/shared/lib/query/engine";
17
+ import { isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
16
18
  import {
17
19
  createSearchTokenAvailability,
18
20
  isSearchFilterOp,
@@ -387,6 +389,34 @@ class HybridQueryEngine {
387
389
  ].filter((filter) => isSearchFilterOp(filter.op));
388
390
  const hasSearchTokens = searchEnabled && searchSources.length && sourceSearchFilters.length ? await this.searchAvailability().anySourceHasTokens(searchSources, opts.tenantId ?? null, orgScope) : false;
389
391
  const searchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens };
392
+ if (searchRuntime.enabled && searchConfig.useIlikeForNonEncryptedFields === true && sourceSearchFilters.some((filter) => !String(filter.field).startsWith("cf:"))) {
393
+ try {
394
+ const encryptionService = this.getEncryptionService();
395
+ const readEncryptedFieldNames = encryptionService?.getEncryptedFieldNames?.bind(encryptionService);
396
+ if (readEncryptedFieldNames) {
397
+ searchRuntime.encryptedFields = await resolveEncryptedLikeFieldSet(
398
+ () => readEncryptedFieldNames(
399
+ entity,
400
+ opts.tenantId ?? null,
401
+ null,
402
+ { ignoreRuntimeHealth: true }
403
+ ),
404
+ String(entity),
405
+ opts.tenantId ?? null
406
+ );
407
+ } else if (isTenantDataEncryptionEnabled()) {
408
+ searchRuntime.encryptedFields = null;
409
+ } else {
410
+ searchRuntime.encryptedFields = /* @__PURE__ */ new Set();
411
+ }
412
+ } catch (err) {
413
+ logger.warn("search: encrypted-field map unavailable; keeping the token rewrite for all columns", {
414
+ entity: String(entity),
415
+ error: err instanceof Error ? err.message : String(err)
416
+ });
417
+ searchRuntime.encryptedFields = null;
418
+ }
419
+ }
390
420
  if (searchFilters.length) {
391
421
  this.logSearchDebug("search:init", {
392
422
  entity,
@@ -1419,7 +1449,7 @@ class HybridQueryEngine {
1419
1449
  if (!baseField) {
1420
1450
  return this.buildIndexDocFilterExpression(eb, "ei", entity, fieldName, filter.op, filter.value, "b.id", searchRuntime);
1421
1451
  }
1422
- if ((filter.op === "like" || filter.op === "ilike") && searchRuntime?.enabled && typeof filter.value === "string") {
1452
+ if ((filter.op === "like" || filter.op === "ilike") && searchRuntime?.enabled && typeof filter.value === "string" && (searchRuntime.encryptedFields == null || isEncryptedLikeField(searchRuntime.encryptedFields, fieldName))) {
1423
1453
  const tokens = tokenizeText(String(filter.value), searchRuntime.config);
1424
1454
  if (tokens.hashes.length) {
1425
1455
  const sources = (searchRuntime.searchSources && searchRuntime.searchSources.length ? searchRuntime.searchSources : [{ entity: String(entity), recordIdColumn: "b.id" }]).filter((src) => src.recordIdColumn && src.entity);
@@ -1439,7 +1469,7 @@ class HybridQueryEngine {
1439
1469
  );
1440
1470
  }
1441
1471
  }
1442
- return sql`true`;
1472
+ return searchRuntime?.encryptedFields != null && isEncryptedLikeField(searchRuntime.encryptedFields, fieldName) ? sql`false` : sql`true`;
1443
1473
  }
1444
1474
  return this.buildColumnFilterExpression(eb, qualify(baseField), filter.op, filter.value);
1445
1475
  }
@@ -1979,7 +2009,10 @@ class HybridQueryEngine {
1979
2009
  logger.debug("Search debug event", { event, payload });
1980
2010
  }
1981
2011
  applyColumnFilter(q, column, filter, search) {
1982
- if ((filter.op === "like" || filter.op === "ilike") && search?.enabled && typeof filter.value === "string") {
2012
+ if ((filter.op === "like" || filter.op === "ilike") && search?.enabled && typeof filter.value === "string" && // Plaintext base columns keep exact SQL ILIKE -- see SearchRuntime.encryptedFields.
2013
+ // Membership runs across name-shape candidates: maps may declare `displayName` while the
2014
+ // filter carries the column name `display_name`.
2015
+ (search.encryptedFields == null || isEncryptedLikeField(search.encryptedFields, search.field))) {
1983
2016
  const tokens = tokenizeText(String(filter.value), search.config);
1984
2017
  const hashes = tokens.hashes;
1985
2018
  if (hashes.length) {
@@ -2009,12 +2042,18 @@ class HybridQueryEngine {
2009
2042
  });
2010
2043
  return q;
2011
2044
  }
2045
+ if (search.encryptedFields != null && isEncryptedLikeField(search.encryptedFields, search.field)) {
2046
+ return q.where(sql`false`);
2047
+ }
2012
2048
  } else {
2013
2049
  this.logSearchDebug("search:skip-empty-hashes", {
2014
2050
  entity: search.entity,
2015
2051
  field: search.field,
2016
2052
  value: filter.value
2017
2053
  });
2054
+ if (search.encryptedFields != null && isEncryptedLikeField(search.encryptedFields, search.field)) {
2055
+ return q.where(sql`false`);
2056
+ }
2018
2057
  }
2019
2058
  return q;
2020
2059
  }