@open-mercato/core 0.4.6-develop-db293c4bbe → 0.4.6-develop-6d72ec5960

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.
@@ -0,0 +1,180 @@
1
+ import { z } from "zod";
2
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
+ import { CustomerTodoLink } from "../../data/entities.js";
5
+ import { createCustomersCrudOpenApi, createPagedListResponseSchema } from "../openapi.js";
6
+ import { decryptEntitiesWithFallbackScope } from "@open-mercato/shared/lib/encryption/subscriber";
7
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
8
+ const querySchema = z.object({
9
+ page: z.coerce.number().min(1).default(1),
10
+ pageSize: z.coerce.number().min(1).max(100).default(50),
11
+ search: z.string().optional(),
12
+ all: z.string().optional()
13
+ });
14
+ const metadata = {
15
+ GET: { requireAuth: true, requireFeatures: ["customers.view"] }
16
+ };
17
+ const TITLE_FIELDS = ["title", "subject", "name", "summary", "text"];
18
+ const IS_DONE_FIELDS = ["is_done", "isDone", "done", "completed"];
19
+ function resolveTodoTitle(raw) {
20
+ for (const key of TITLE_FIELDS) {
21
+ const value = raw[key];
22
+ if (typeof value === "string" && value.trim()) return value.trim();
23
+ }
24
+ return null;
25
+ }
26
+ function resolveTodoIsDone(raw) {
27
+ for (const key of IS_DONE_FIELDS) {
28
+ const value = raw[key];
29
+ if (typeof value === "boolean") return value;
30
+ }
31
+ return null;
32
+ }
33
+ async function resolveTodoSummaries(queryEngine, links, tenantId, orgId) {
34
+ const results = /* @__PURE__ */ new Map();
35
+ if (!links.length || !tenantId) return results;
36
+ const idsBySource = /* @__PURE__ */ new Map();
37
+ for (const link of links) {
38
+ if (!link.todoSource || !link.todoId) continue;
39
+ if (!idsBySource.has(link.todoSource)) idsBySource.set(link.todoSource, /* @__PURE__ */ new Set());
40
+ idsBySource.get(link.todoSource).add(link.todoId);
41
+ }
42
+ const requestedFields = ["id", ...TITLE_FIELDS, ...IS_DONE_FIELDS];
43
+ const organizationIds = orgId ? [orgId] : void 0;
44
+ for (const [source, idSet] of idsBySource.entries()) {
45
+ const ids = Array.from(idSet);
46
+ try {
47
+ const result = await queryEngine.query(source, {
48
+ tenantId,
49
+ organizationIds,
50
+ filters: { id: { $in: ids } },
51
+ fields: requestedFields,
52
+ includeCustomFields: false,
53
+ page: { page: 1, pageSize: Math.max(ids.length, 1) }
54
+ });
55
+ for (const item of result.items ?? []) {
56
+ const raw = item;
57
+ const todoId = typeof raw.id === "string" ? raw.id : String(raw.id ?? "");
58
+ if (!todoId) continue;
59
+ results.set(`${source}:${todoId}`, {
60
+ title: resolveTodoTitle(raw),
61
+ isDone: resolveTodoIsDone(raw)
62
+ });
63
+ }
64
+ } catch {
65
+ }
66
+ }
67
+ return results;
68
+ }
69
+ async function GET(request) {
70
+ const auth = await getAuthFromRequest(request);
71
+ if (!auth?.sub && !auth?.isApiKey) {
72
+ return new Response(JSON.stringify({ error: "Authentication required" }), {
73
+ status: 401,
74
+ headers: { "Content-Type": "application/json" }
75
+ });
76
+ }
77
+ const url = new URL(request.url);
78
+ const parsed = querySchema.safeParse({
79
+ page: url.searchParams.get("page") ?? void 0,
80
+ pageSize: url.searchParams.get("pageSize") ?? void 0,
81
+ search: url.searchParams.get("search") ?? void 0,
82
+ all: url.searchParams.get("all") ?? void 0
83
+ });
84
+ if (!parsed.success) {
85
+ return new Response(JSON.stringify({ error: "Invalid parameters" }), {
86
+ status: 400,
87
+ headers: { "Content-Type": "application/json" }
88
+ });
89
+ }
90
+ const { page, pageSize, search, all } = parsed.data;
91
+ const exportAll = parseBooleanToken(all);
92
+ const container = await createRequestContainer();
93
+ const em = container.resolve("em");
94
+ const where = {
95
+ tenantId: auth.tenantId
96
+ };
97
+ if (auth.orgId) {
98
+ where.organizationId = auth.orgId;
99
+ }
100
+ if (search?.trim()) {
101
+ where.entity = { displayName: { $ilike: `%${search.trim()}%` } };
102
+ }
103
+ const [links, total] = await em.findAndCount(
104
+ CustomerTodoLink,
105
+ where,
106
+ {
107
+ populate: ["entity"],
108
+ orderBy: { createdAt: "desc" },
109
+ ...exportAll ? {} : {
110
+ offset: (page - 1) * pageSize,
111
+ limit: pageSize
112
+ }
113
+ }
114
+ );
115
+ await decryptEntitiesWithFallbackScope(links, {
116
+ em,
117
+ tenantId: auth.tenantId,
118
+ organizationId: auth.orgId ?? null
119
+ });
120
+ const queryEngine = container.resolve("queryEngine");
121
+ const todoSummaries = await resolveTodoSummaries(queryEngine, links, auth.tenantId, auth.orgId ?? null);
122
+ const effectivePage = exportAll ? 1 : page;
123
+ const effectivePageSize = exportAll ? total : pageSize;
124
+ const items = links.map((link) => {
125
+ const summary = todoSummaries.get(`${link.todoSource}:${link.todoId}`) ?? null;
126
+ return {
127
+ id: link.id,
128
+ todoId: link.todoId,
129
+ todoSource: link.todoSource,
130
+ todoTitle: summary?.title ?? null,
131
+ todoIsDone: summary?.isDone ?? null,
132
+ todoOrganizationId: link.organizationId,
133
+ organizationId: link.organizationId,
134
+ tenantId: link.tenantId,
135
+ createdAt: link.createdAt.toISOString(),
136
+ customer: {
137
+ id: link.entity.id,
138
+ displayName: link.entity.displayName,
139
+ kind: link.entity.kind
140
+ }
141
+ };
142
+ });
143
+ return new Response(
144
+ JSON.stringify({
145
+ items,
146
+ total,
147
+ page: effectivePage,
148
+ pageSize: effectivePageSize,
149
+ totalPages: exportAll ? 1 : Math.ceil(total / pageSize)
150
+ }),
151
+ { status: 200, headers: { "Content-Type": "application/json" } }
152
+ );
153
+ }
154
+ const todoItemSchema = z.object({
155
+ id: z.string(),
156
+ todoId: z.string(),
157
+ todoSource: z.string(),
158
+ todoTitle: z.string().nullable(),
159
+ todoIsDone: z.boolean().nullable(),
160
+ todoOrganizationId: z.string().nullable(),
161
+ organizationId: z.string(),
162
+ tenantId: z.string(),
163
+ createdAt: z.string(),
164
+ customer: z.object({
165
+ id: z.string().nullable(),
166
+ displayName: z.string().nullable(),
167
+ kind: z.string().nullable()
168
+ })
169
+ });
170
+ const openApi = createCustomersCrudOpenApi({
171
+ resourceName: "CustomerTodo",
172
+ querySchema,
173
+ listResponseSchema: createPagedListResponseSchema(todoItemSchema)
174
+ });
175
+ export {
176
+ GET,
177
+ metadata,
178
+ openApi
179
+ };
180
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/customers/api/todos/route.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomerTodoLink } from '../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createCustomersCrudOpenApi, createPagedListResponseSchema } from '../openapi'\nimport { decryptEntitiesWithFallbackScope } from '@open-mercato/shared/lib/encryption/subscriber'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n all: z.string().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.view'] },\n}\n\nconst TITLE_FIELDS = ['title', 'subject', 'name', 'summary', 'text'] as const\nconst IS_DONE_FIELDS = ['is_done', 'isDone', 'done', 'completed'] as const\n\nfunction resolveTodoTitle(raw: Record<string, unknown>): string | null {\n for (const key of TITLE_FIELDS) {\n const value = raw[key]\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return null\n}\n\nfunction resolveTodoIsDone(raw: Record<string, unknown>): boolean | null {\n for (const key of IS_DONE_FIELDS) {\n const value = raw[key]\n if (typeof value === 'boolean') return value\n }\n return null\n}\n\nasync function resolveTodoSummaries(\n queryEngine: QueryEngine,\n links: CustomerTodoLink[],\n tenantId: string | null,\n orgId: string | null,\n): Promise<Map<string, { title: string | null; isDone: boolean | null }>> {\n const results = new Map<string, { title: string | null; isDone: boolean | null }>()\n if (!links.length || !tenantId) return results\n\n const idsBySource = new Map<string, Set<string>>()\n for (const link of links) {\n if (!link.todoSource || !link.todoId) continue\n if (!idsBySource.has(link.todoSource)) idsBySource.set(link.todoSource, new Set())\n idsBySource.get(link.todoSource)!.add(link.todoId)\n }\n\n const requestedFields = ['id', ...TITLE_FIELDS, ...IS_DONE_FIELDS]\n const organizationIds = orgId ? [orgId] : undefined\n\n for (const [source, idSet] of idsBySource.entries()) {\n const ids = Array.from(idSet)\n try {\n const result = await queryEngine.query<Record<string, unknown>>(source as EntityId, {\n tenantId,\n organizationIds,\n filters: { id: { $in: ids } },\n fields: requestedFields,\n includeCustomFields: false,\n page: { page: 1, pageSize: Math.max(ids.length, 1) },\n })\n for (const item of result.items ?? []) {\n const raw = item as Record<string, unknown>\n const todoId = typeof raw.id === 'string' ? raw.id : String(raw.id ?? '')\n if (!todoId) continue\n results.set(`${source}:${todoId}`, {\n title: resolveTodoTitle(raw),\n isDone: resolveTodoIsDone(raw),\n })\n }\n } catch {\n // non-critical: todo metadata unavailable, items fall back to null\n }\n }\n\n return results\n}\n\nexport async function GET(request: Request): Promise<Response> {\n const auth = await getAuthFromRequest(request)\n if (!auth?.sub && !auth?.isApiKey) {\n return new Response(JSON.stringify({ error: 'Authentication required' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const url = new URL(request.url)\n const parsed = querySchema.safeParse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n all: url.searchParams.get('all') ?? undefined,\n })\n\n if (!parsed.success) {\n return new Response(JSON.stringify({ error: 'Invalid parameters' }), {\n status: 400,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const { page, pageSize, search, all } = parsed.data\n const exportAll = parseBooleanToken(all)\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n where.organizationId = auth.orgId\n }\n\n if (search?.trim()) {\n where.entity = { displayName: { $ilike: `%${search.trim()}%` } }\n }\n\n const [links, total] = await em.findAndCount(\n CustomerTodoLink,\n where,\n {\n populate: ['entity'],\n orderBy: { createdAt: 'desc' },\n ...(exportAll ? {} : {\n offset: (page - 1) * pageSize,\n limit: pageSize,\n }),\n },\n )\n\n await decryptEntitiesWithFallbackScope(links, {\n em,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const todoSummaries = await resolveTodoSummaries(queryEngine, links, auth.tenantId, auth.orgId ?? null)\n\n const effectivePage = exportAll ? 1 : page\n const effectivePageSize = exportAll ? total : pageSize\n\n const items = links.map((link) => {\n const summary = todoSummaries.get(`${link.todoSource}:${link.todoId}`) ?? null\n return {\n id: link.id,\n todoId: link.todoId,\n todoSource: link.todoSource,\n todoTitle: summary?.title ?? null,\n todoIsDone: summary?.isDone ?? null,\n todoOrganizationId: link.organizationId,\n organizationId: link.organizationId,\n tenantId: link.tenantId,\n createdAt: link.createdAt.toISOString(),\n customer: {\n id: link.entity.id,\n displayName: link.entity.displayName,\n kind: link.entity.kind,\n },\n }\n })\n\n return new Response(\n JSON.stringify({\n items,\n total,\n page: effectivePage,\n pageSize: effectivePageSize,\n totalPages: exportAll ? 1 : Math.ceil(total / pageSize),\n }),\n { status: 200, headers: { 'Content-Type': 'application/json' } },\n )\n}\n\nconst todoItemSchema = z.object({\n id: z.string(),\n todoId: z.string(),\n todoSource: z.string(),\n todoTitle: z.string().nullable(),\n todoIsDone: z.boolean().nullable(),\n todoOrganizationId: z.string().nullable(),\n organizationId: z.string(),\n tenantId: z.string(),\n createdAt: z.string(),\n customer: z.object({\n id: z.string().nullable(),\n displayName: z.string().nullable(),\n kind: z.string().nullable(),\n }),\n})\n\nexport const openApi: OpenApiRouteDoc = createCustomersCrudOpenApi({\n resourceName: 'CustomerTodo',\n querySchema,\n listResponseSchema: createPagedListResponseSchema(todoItemSchema),\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,wBAAwB;AAEjC,SAAS,4BAA4B,qCAAqC;AAC1E,SAAS,wCAAwC;AAGjD,SAAS,yBAAyB;AAElC,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,KAAK,EAAE,OAAO,EAAE,SAAS;AAC3B,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAChE;AAEA,MAAM,eAAe,CAAC,SAAS,WAAW,QAAQ,WAAW,MAAM;AACnE,MAAM,iBAAiB,CAAC,WAAW,UAAU,QAAQ,WAAW;AAEhE,SAAS,iBAAiB,KAA6C;AACrE,aAAW,OAAO,cAAc;AAC9B,UAAM,QAAQ,IAAI,GAAG;AACrB,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAG,QAAO,MAAM,KAAK;AAAA,EACnE;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,KAA8C;AACvE,aAAW,OAAO,gBAAgB;AAChC,UAAM,QAAQ,IAAI,GAAG;AACrB,QAAI,OAAO,UAAU,UAAW,QAAO;AAAA,EACzC;AACA,SAAO;AACT;AAEA,eAAe,qBACb,aACA,OACA,UACA,OACwE;AACxE,QAAM,UAAU,oBAAI,IAA8D;AAClF,MAAI,CAAC,MAAM,UAAU,CAAC,SAAU,QAAO;AAEvC,QAAM,cAAc,oBAAI,IAAyB;AACjD,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,cAAc,CAAC,KAAK,OAAQ;AACtC,QAAI,CAAC,YAAY,IAAI,KAAK,UAAU,EAAG,aAAY,IAAI,KAAK,YAAY,oBAAI,IAAI,CAAC;AACjF,gBAAY,IAAI,KAAK,UAAU,EAAG,IAAI,KAAK,MAAM;AAAA,EACnD;AAEA,QAAM,kBAAkB,CAAC,MAAM,GAAG,cAAc,GAAG,cAAc;AACjE,QAAM,kBAAkB,QAAQ,CAAC,KAAK,IAAI;AAE1C,aAAW,CAAC,QAAQ,KAAK,KAAK,YAAY,QAAQ,GAAG;AACnD,UAAM,MAAM,MAAM,KAAK,KAAK;AAC5B,QAAI;AACF,YAAM,SAAS,MAAM,YAAY,MAA+B,QAAoB;AAAA,QAClF;AAAA,QACA;AAAA,QACA,SAAS,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE;AAAA,QAC5B,QAAQ;AAAA,QACR,qBAAqB;AAAA,QACrB,MAAM,EAAE,MAAM,GAAG,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,EAAE;AAAA,MACrD,CAAC;AACD,iBAAW,QAAQ,OAAO,SAAS,CAAC,GAAG;AACrC,cAAM,MAAM;AACZ,cAAM,SAAS,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK,OAAO,IAAI,MAAM,EAAE;AACxE,YAAI,CAAC,OAAQ;AACb,gBAAQ,IAAI,GAAG,MAAM,IAAI,MAAM,IAAI;AAAA,UACjC,OAAO,iBAAiB,GAAG;AAAA,UAC3B,QAAQ,kBAAkB,GAAG;AAAA,QAC/B,CAAC;AAAA,MACH;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAsB,IAAI,SAAqC;AAC7D,QAAM,OAAO,MAAM,mBAAmB,OAAO;AAC7C,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,0BAA0B,CAAC,GAAG;AAAA,MACxE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC;AAAA,EACH;AAEA,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,QAAM,SAAS,YAAY,UAAU;AAAA,IACnC,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,KAAK,IAAI,aAAa,IAAI,KAAK,KAAK;AAAA,EACtC,CAAC;AAED,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,qBAAqB,CAAC,GAAG;AAAA,MACnE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC;AAAA,EACH;AAEA,QAAM,EAAE,MAAM,UAAU,QAAQ,IAAI,IAAI,OAAO;AAC/C,QAAM,YAAY,kBAAkB,GAAG;AAEvC,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,QAAM,QAAiC;AAAA,IACrC,UAAU,KAAK;AAAA,EACjB;AACA,MAAI,KAAK,OAAO;AACd,UAAM,iBAAiB,KAAK;AAAA,EAC9B;AAEA,MAAI,QAAQ,KAAK,GAAG;AAClB,UAAM,SAAS,EAAE,aAAa,EAAE,QAAQ,IAAI,OAAO,KAAK,CAAC,IAAI,EAAE;AAAA,EACjE;AAEA,QAAM,CAAC,OAAO,KAAK,IAAI,MAAM,GAAG;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,CAAC,QAAQ;AAAA,MACnB,SAAS,EAAE,WAAW,OAAO;AAAA,MAC7B,GAAI,YAAY,CAAC,IAAI;AAAA,QACnB,SAAS,OAAO,KAAK;AAAA,QACrB,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iCAAiC,OAAO;AAAA,IAC5C;AAAA,IACA,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,SAAS;AAAA,EAChC,CAAC;AAED,QAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,QAAM,gBAAgB,MAAM,qBAAqB,aAAa,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI;AAEtG,QAAM,gBAAgB,YAAY,IAAI;AACtC,QAAM,oBAAoB,YAAY,QAAQ;AAE9C,QAAM,QAAQ,MAAM,IAAI,CAAC,SAAS;AAChC,UAAM,UAAU,cAAc,IAAI,GAAG,KAAK,UAAU,IAAI,KAAK,MAAM,EAAE,KAAK;AAC1E,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,QAAQ,KAAK;AAAA,MACb,YAAY,KAAK;AAAA,MACjB,WAAW,SAAS,SAAS;AAAA,MAC7B,YAAY,SAAS,UAAU;AAAA,MAC/B,oBAAoB,KAAK;AAAA,MACzB,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,WAAW,KAAK,UAAU,YAAY;AAAA,MACtC,UAAU;AAAA,QACR,IAAI,KAAK,OAAO;AAAA,QAChB,aAAa,KAAK,OAAO;AAAA,QACzB,MAAM,KAAK,OAAO;AAAA,MACpB;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,IAAI;AAAA,IACT,KAAK,UAAU;AAAA,MACb;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,UAAU;AAAA,MACV,YAAY,YAAY,IAAI,KAAK,KAAK,QAAQ,QAAQ;AAAA,IACxD,CAAC;AAAA,IACD,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,mBAAmB,EAAE;AAAA,EACjE;AACF;AAEA,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,OAAO;AAAA,EACb,QAAQ,EAAE,OAAO;AAAA,EACjB,YAAY,EAAE,OAAO;AAAA,EACrB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,YAAY,EAAE,QAAQ,EAAE,SAAS;AAAA,EACjC,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,EACxC,gBAAgB,EAAE,OAAO;AAAA,EACzB,UAAU,EAAE,OAAO;AAAA,EACnB,WAAW,EAAE,OAAO;AAAA,EACpB,UAAU,EAAE,OAAO;AAAA,IACjB,IAAI,EAAE,OAAO,EAAE,SAAS;AAAA,IACxB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,IACjC,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,CAAC;AACH,CAAC;AAEM,MAAM,UAA2B,2BAA2B;AAAA,EACjE,cAAc;AAAA,EACd;AAAA,EACA,oBAAoB,8BAA8B,cAAc;AAClE,CAAC;",
6
+ "names": []
7
+ }
@@ -1,9 +1,9 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import Link from "next/link";
5
5
  import { useRouter } from "next/navigation";
6
- import { useQuery } from "@tanstack/react-query";
6
+ import { useQuery, keepPreviousData } from "@tanstack/react-query";
7
7
  import { DataTable } from "@open-mercato/ui/backend/DataTable";
8
8
  import { RowActions } from "@open-mercato/ui/backend/RowActions";
9
9
  import { BooleanIcon } from "@open-mercato/ui/backend/ValueIcons";
@@ -28,17 +28,14 @@ function CustomerTodosTable() {
28
28
  const [search, setSearch] = React.useState("");
29
29
  const [page, setPage] = React.useState(1);
30
30
  const [pageSize] = React.useState(50);
31
- const [filters, setFilters] = React.useState({});
32
31
  const params = React.useMemo(() => {
33
32
  const usp = new URLSearchParams({
34
33
  page: String(page),
35
34
  pageSize: String(pageSize)
36
35
  });
37
36
  if (search.trim().length > 0) usp.set("search", search.trim());
38
- const doneValue = filters.is_done;
39
- if (doneValue === "true" || doneValue === "false") usp.set("isDone", doneValue);
40
37
  return usp.toString();
41
- }, [page, pageSize, search, filters]);
38
+ }, [page, pageSize, search]);
42
39
  const columns = React.useMemo(() => [
43
40
  {
44
41
  accessorKey: "customer.displayName",
@@ -87,7 +84,8 @@ function CustomerTodosTable() {
87
84
  void 0,
88
85
  { errorMessage: t("customers.workPlan.customerTodos.table.error.load") }
89
86
  );
90
- }
87
+ },
88
+ placeholderData: keepPreviousData
91
89
  });
92
90
  const rows = data?.items ?? [];
93
91
  const exportConfig = React.useMemo(() => ({
@@ -114,30 +112,6 @@ function CustomerTodosTable() {
114
112
  filename: () => "customer_todos_full"
115
113
  }
116
114
  }), [rows, t, viewExportColumns]);
117
- const filterDefs = React.useMemo(() => [
118
- {
119
- id: "is_done",
120
- label: t("customers.workPlan.customerTodos.table.filters.done"),
121
- type: "select",
122
- options: [
123
- { label: t("customers.workPlan.customerTodos.table.filters.doneOption.any"), value: "" },
124
- { label: t("customers.workPlan.customerTodos.table.filters.doneOption.open"), value: "false" },
125
- { label: t("customers.workPlan.customerTodos.table.filters.doneOption.completed"), value: "true" }
126
- ]
127
- }
128
- ], [t]);
129
- const onFiltersApply = React.useCallback((next) => {
130
- const nextValue = next?.is_done;
131
- setFilters((prev) => {
132
- if (prev.is_done === nextValue) return prev;
133
- return { is_done: nextValue };
134
- });
135
- setPage(1);
136
- }, []);
137
- const onFiltersClear = React.useCallback(() => {
138
- setFilters({});
139
- setPage(1);
140
- }, []);
141
115
  const handleRefresh = React.useCallback(async () => {
142
116
  try {
143
117
  await refetch();
@@ -153,66 +127,60 @@ function CustomerTodosTable() {
153
127
  router.push(href);
154
128
  }, [router]);
155
129
  const errorMessage = error ? error instanceof Error ? error.message : t("customers.workPlan.customerTodos.table.error.load") : null;
156
- const isEmpty = !isLoading && !errorMessage && rows.length === 0;
157
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
158
- /* @__PURE__ */ jsx(
159
- DataTable,
160
- {
161
- title: t("customers.workPlan.customerTodos.table.title"),
162
- actions: /* @__PURE__ */ jsx(
163
- Button,
130
+ const emptyStateMessage = !isLoading && !errorMessage && rows.length === 0 ? search ? t("customers.workPlan.customerTodos.table.state.noMatches") : t("customers.workPlan.customerTodos.table.state.empty") : void 0;
131
+ return /* @__PURE__ */ jsx(
132
+ DataTable,
133
+ {
134
+ title: t("customers.workPlan.customerTodos.table.title"),
135
+ actions: /* @__PURE__ */ jsx(
136
+ Button,
137
+ {
138
+ variant: "outline",
139
+ onClick: () => {
140
+ void handleRefresh();
141
+ },
142
+ disabled: isFetching,
143
+ children: t("customers.workPlan.customerTodos.table.actions.refresh")
144
+ }
145
+ ),
146
+ columns,
147
+ data: rows,
148
+ exporter: exportConfig,
149
+ searchValue: search,
150
+ onSearchChange: (value) => {
151
+ setSearch(value);
152
+ setPage(1);
153
+ },
154
+ perspective: { tableId: "customers.todos.list" },
155
+ rowActions: (row) => {
156
+ const customerLink = buildCustomerHref(row);
157
+ if (!customerLink) return null;
158
+ return /* @__PURE__ */ jsx(
159
+ RowActions,
164
160
  {
165
- variant: "outline",
166
- onClick: () => {
167
- void handleRefresh();
168
- },
169
- disabled: isFetching,
170
- children: t("customers.workPlan.customerTodos.table.actions.refresh")
161
+ items: [
162
+ {
163
+ id: "open-customer",
164
+ label: t("customers.workPlan.customerTodos.table.actions.openCustomer"),
165
+ href: customerLink
166
+ }
167
+ ]
171
168
  }
172
- ),
173
- columns,
174
- data: rows,
175
- exporter: exportConfig,
176
- searchValue: search,
177
- onSearchChange: (value) => {
178
- setSearch(value);
179
- setPage(1);
180
- },
181
- perspective: { tableId: "customers.todos.list" },
182
- filters: filterDefs,
183
- filterValues: filters,
184
- onFiltersApply,
185
- onFiltersClear,
186
- rowActions: (row) => {
187
- const customerLink = buildCustomerHref(row);
188
- if (!customerLink) return null;
189
- return /* @__PURE__ */ jsx(
190
- RowActions,
191
- {
192
- items: [
193
- {
194
- id: "open-customer",
195
- label: t("customers.workPlan.customerTodos.table.actions.openCustomer"),
196
- href: customerLink
197
- }
198
- ]
199
- }
200
- );
201
- },
202
- onRowClick: handleNavigate,
203
- pagination: {
204
- page,
205
- pageSize,
206
- total: data?.total ?? 0,
207
- totalPages: data?.totalPages ?? 0,
208
- onPageChange: setPage
209
- },
210
- isLoading
211
- }
212
- ),
213
- errorMessage ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive", children: errorMessage }) : null,
214
- isEmpty ? /* @__PURE__ */ jsx("div", { className: "py-8 text-sm text-muted-foreground", children: search || filters.is_done ? t("customers.workPlan.customerTodos.table.state.noMatches") : t("customers.workPlan.customerTodos.table.state.empty") }) : null
215
- ] });
169
+ );
170
+ },
171
+ onRowClick: handleNavigate,
172
+ pagination: {
173
+ page,
174
+ pageSize,
175
+ total: data?.total ?? 0,
176
+ totalPages: data?.totalPages ?? 0,
177
+ onPageChange: setPage
178
+ },
179
+ isLoading,
180
+ error: errorMessage,
181
+ emptyState: emptyStateMessage
182
+ }
183
+ );
216
184
  }
217
185
  var CustomerTodosTable_default = CustomerTodosTable;
218
186
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/components/CustomerTodosTable.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useQuery } from '@tanstack/react-query'\nimport { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'\nimport type { PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildCrudExportUrl } from '@open-mercato/ui/backend/utils/crud'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype CustomerTodoItem = {\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 organizationId: string\n tenantId: string\n createdAt: string\n customer: {\n id: string | null\n displayName: string | null\n kind: string | null\n }\n}\n\ntype CustomerTodosResponse = {\n items: CustomerTodoItem[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nconst TASKS_TAB_QUERY = 'tab=tasks'\n\nfunction buildCustomerHref(item: CustomerTodoItem): string | null {\n const customerId = item.customer?.id\n if (!customerId) return null\n const kind = (item.customer?.kind ?? '').toLowerCase()\n const base =\n kind === 'company'\n ? `/backend/customers/companies/${customerId}`\n : `/backend/customers/people/${customerId}`\n return `${base}?${TASKS_TAB_QUERY}`\n}\n\nexport function CustomerTodosTable(): React.JSX.Element {\n const t = useT()\n const router = useRouter()\n const scopeVersion = useOrganizationScopeVersion()\n\n const [search, setSearch] = React.useState('')\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [filters, setFilters] = React.useState<FilterValues>({})\n\n const params = React.useMemo(() => {\n const usp = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n })\n if (search.trim().length > 0) usp.set('search', search.trim())\n const doneValue = filters.is_done\n if (doneValue === 'true' || doneValue === 'false') usp.set('isDone', doneValue)\n return usp.toString()\n }, [page, pageSize, search, filters])\n\n const columns = React.useMemo<ColumnDef<CustomerTodoItem>[]>(() => [\n {\n accessorKey: 'customer.displayName',\n header: t('customers.workPlan.customerTodos.table.column.customer'),\n cell: ({ row }) => {\n const name = row.original.customer?.displayName\n if (!name) return <span className=\"text-muted-foreground\">\u2014</span>\n const href = buildCustomerHref(row.original)\n if (!href) return name\n return (\n <Link href={href} className=\"underline-offset-2 hover:underline\">\n {name}\n </Link>\n )\n },\n meta: { priority: 1 },\n },\n {\n accessorKey: 'todoTitle',\n header: t('customers.workPlan.customerTodos.table.column.todo'),\n cell: ({ row }) => {\n const title = row.original.todoTitle ?? t('customers.workPlan.customerTodos.table.column.todo.unnamed')\n const todoId = row.original.todoId\n if (!todoId) return <span className=\"text-muted-foreground\">{title}</span>\n return (\n <Link href={`/backend/todos/${todoId}/edit`} className=\"underline-offset-2 hover:underline\">\n {title}\n </Link>\n )\n },\n meta: { priority: 2 },\n },\n {\n accessorKey: 'todoIsDone',\n header: t('customers.workPlan.customerTodos.table.column.done'),\n cell: ({ row }) => <BooleanIcon value={row.original.todoIsDone === true} />,\n meta: { priority: 3 },\n },\n ], [t])\n\n const viewExportColumns = React.useMemo(() => {\n return columns\n .map((col) => {\n const accessorKey = (col as any).accessorKey\n if (!accessorKey || typeof accessorKey !== 'string') return null\n if ((col as any).meta?.hidden) return null\n const header = typeof col.header === 'string'\n ? col.header\n : accessorKey\n return { field: accessorKey, header }\n })\n .filter((col): col is { field: string; header: string } => !!col)\n }, [columns])\n\n const { data, isLoading, error, refetch, isFetching } = useQuery<CustomerTodosResponse>({\n queryKey: ['customers-todos', params, scopeVersion],\n queryFn: async () => {\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params}`,\n undefined,\n { errorMessage: t('customers.workPlan.customerTodos.table.error.load') },\n )\n },\n })\n\n const rows = data?.items ?? []\n\n const exportConfig = React.useMemo(() => ({\n view: {\n description: t('customers.workPlan.customerTodos.table.export.view'),\n prepare: async (): Promise<{ prepared: PreparedExport; filename: string }> => {\n const rowsForExport = rows.map((row) => {\n const out: Record<string, unknown> = {}\n for (const col of viewExportColumns) {\n out[col.field] = (row as Record<string, unknown>)[col.field]\n }\n return out\n })\n const prepared: PreparedExport = {\n columns: viewExportColumns.map((col) => ({ field: col.field, header: col.header })),\n rows: rowsForExport,\n }\n return { prepared, filename: 'customer_todos_view' }\n },\n },\n full: {\n description: t('customers.workPlan.customerTodos.table.export.full'),\n getUrl: (format: DataTableExportFormat) =>\n buildCrudExportUrl('customers/todos', { exportScope: 'full', all: 'true' }, format),\n filename: () => 'customer_todos_full',\n },\n }), [rows, t, viewExportColumns])\n\n const filterDefs = React.useMemo<FilterDef[]>(() => [\n {\n id: 'is_done',\n label: t('customers.workPlan.customerTodos.table.filters.done'),\n type: 'select',\n options: [\n { label: t('customers.workPlan.customerTodos.table.filters.doneOption.any'), value: '' },\n { label: t('customers.workPlan.customerTodos.table.filters.doneOption.open'), value: 'false' },\n { label: t('customers.workPlan.customerTodos.table.filters.doneOption.completed'), value: 'true' },\n ],\n },\n ], [t])\n\n const onFiltersApply = React.useCallback((next: FilterValues) => {\n const nextValue = next?.is_done\n setFilters((prev) => {\n if (prev.is_done === nextValue) return prev\n return { is_done: nextValue }\n })\n setPage(1)\n }, [])\n\n const onFiltersClear = React.useCallback(() => {\n setFilters({})\n setPage(1)\n }, [])\n\n const handleRefresh = React.useCallback(async () => {\n try {\n await refetch()\n flash(t('customers.workPlan.customerTodos.table.flash.refreshed'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customers.workPlan.customerTodos.table.error.load')\n flash(message, 'error')\n }\n }, [refetch, t])\n\n const handleNavigate = React.useCallback((item: CustomerTodoItem) => {\n const href = buildCustomerHref(item)\n if (!href) return\n router.push(href)\n }, [router])\n\n const errorMessage = error ? (error instanceof Error ? error.message : t('customers.workPlan.customerTodos.table.error.load')) : null\n const isEmpty = !isLoading && !errorMessage && rows.length === 0\n\n return (\n <div className=\"space-y-4\">\n <DataTable\n title={t('customers.workPlan.customerTodos.table.title')}\n actions={(\n <Button\n variant=\"outline\"\n onClick={() => { void handleRefresh() }}\n disabled={isFetching}\n >\n {t('customers.workPlan.customerTodos.table.actions.refresh')}\n </Button>\n )}\n columns={columns}\n data={rows}\n exporter={exportConfig}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n perspective={{ tableId: 'customers.todos.list' }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={onFiltersApply}\n onFiltersClear={onFiltersClear}\n rowActions={(row) => {\n const customerLink = buildCustomerHref(row)\n if (!customerLink) return null\n return (\n <RowActions\n items={[\n {\n id: 'open-customer',\n label: t('customers.workPlan.customerTodos.table.actions.openCustomer'),\n href: customerLink,\n },\n ]}\n />\n )\n }}\n onRowClick={handleNavigate}\n pagination={{\n page,\n pageSize,\n total: data?.total ?? 0,\n totalPages: data?.totalPages ?? 0,\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n />\n {errorMessage ? (\n <div className=\"rounded-md border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive\">\n {errorMessage}\n </div>\n ) : null}\n {isEmpty ? (\n <div className=\"py-8 text-sm text-muted-foreground\">\n {search || filters.is_done\n ? t('customers.workPlan.customerTodos.table.state.noMatches')\n : t('customers.workPlan.customerTodos.table.state.empty')}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport default CustomerTodosTable\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useQuery, keepPreviousData } from '@tanstack/react-query'\nimport { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'\nimport type { PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildCrudExportUrl } from '@open-mercato/ui/backend/utils/crud'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype CustomerTodoItem = {\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 organizationId: string\n tenantId: string\n createdAt: string\n customer: {\n id: string | null\n displayName: string | null\n kind: string | null\n }\n}\n\ntype CustomerTodosResponse = {\n items: CustomerTodoItem[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\nconst TASKS_TAB_QUERY = 'tab=tasks'\n\nfunction buildCustomerHref(item: CustomerTodoItem): string | null {\n const customerId = item.customer?.id\n if (!customerId) return null\n const kind = (item.customer?.kind ?? '').toLowerCase()\n const base =\n kind === 'company'\n ? `/backend/customers/companies/${customerId}`\n : `/backend/customers/people/${customerId}`\n return `${base}?${TASKS_TAB_QUERY}`\n}\n\nexport function CustomerTodosTable(): React.JSX.Element {\n const t = useT()\n const router = useRouter()\n const scopeVersion = useOrganizationScopeVersion()\n\n const [search, setSearch] = React.useState('')\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n\n const params = React.useMemo(() => {\n const usp = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n })\n if (search.trim().length > 0) usp.set('search', search.trim())\n return usp.toString()\n }, [page, pageSize, search])\n\n const columns = React.useMemo<ColumnDef<CustomerTodoItem>[]>(() => [\n {\n accessorKey: 'customer.displayName',\n header: t('customers.workPlan.customerTodos.table.column.customer'),\n cell: ({ row }) => {\n const name = row.original.customer?.displayName\n if (!name) return <span className=\"text-muted-foreground\">\u2014</span>\n const href = buildCustomerHref(row.original)\n if (!href) return name\n return (\n <Link href={href} className=\"underline-offset-2 hover:underline\">\n {name}\n </Link>\n )\n },\n meta: { priority: 1 },\n },\n {\n accessorKey: 'todoTitle',\n header: t('customers.workPlan.customerTodos.table.column.todo'),\n cell: ({ row }) => {\n const title = row.original.todoTitle ?? t('customers.workPlan.customerTodos.table.column.todo.unnamed')\n const todoId = row.original.todoId\n if (!todoId) return <span className=\"text-muted-foreground\">{title}</span>\n return (\n <Link href={`/backend/todos/${todoId}/edit`} className=\"underline-offset-2 hover:underline\">\n {title}\n </Link>\n )\n },\n meta: { priority: 2 },\n },\n {\n accessorKey: 'todoIsDone',\n header: t('customers.workPlan.customerTodos.table.column.done'),\n cell: ({ row }) => <BooleanIcon value={row.original.todoIsDone === true} />,\n meta: { priority: 3 },\n },\n ], [t])\n\n const viewExportColumns = React.useMemo(() => {\n return columns\n .map((col) => {\n const accessorKey = (col as any).accessorKey\n if (!accessorKey || typeof accessorKey !== 'string') return null\n if ((col as any).meta?.hidden) return null\n const header = typeof col.header === 'string'\n ? col.header\n : accessorKey\n return { field: accessorKey, header }\n })\n .filter((col): col is { field: string; header: string } => !!col)\n }, [columns])\n\n const { data, isLoading, error, refetch, isFetching } = useQuery<CustomerTodosResponse>({\n queryKey: ['customers-todos', params, scopeVersion],\n queryFn: async () => {\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params}`,\n undefined,\n { errorMessage: t('customers.workPlan.customerTodos.table.error.load') },\n )\n },\n placeholderData: keepPreviousData,\n })\n\n const rows = data?.items ?? []\n\n const exportConfig = React.useMemo(() => ({\n view: {\n description: t('customers.workPlan.customerTodos.table.export.view'),\n prepare: async (): Promise<{ prepared: PreparedExport; filename: string }> => {\n const rowsForExport = rows.map((row) => {\n const out: Record<string, unknown> = {}\n for (const col of viewExportColumns) {\n out[col.field] = (row as Record<string, unknown>)[col.field]\n }\n return out\n })\n const prepared: PreparedExport = {\n columns: viewExportColumns.map((col) => ({ field: col.field, header: col.header })),\n rows: rowsForExport,\n }\n return { prepared, filename: 'customer_todos_view' }\n },\n },\n full: {\n description: t('customers.workPlan.customerTodos.table.export.full'),\n getUrl: (format: DataTableExportFormat) =>\n buildCrudExportUrl('customers/todos', { exportScope: 'full', all: 'true' }, format),\n filename: () => 'customer_todos_full',\n },\n }), [rows, t, viewExportColumns])\n\n const handleRefresh = React.useCallback(async () => {\n try {\n await refetch()\n flash(t('customers.workPlan.customerTodos.table.flash.refreshed'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customers.workPlan.customerTodos.table.error.load')\n flash(message, 'error')\n }\n }, [refetch, t])\n\n const handleNavigate = React.useCallback((item: CustomerTodoItem) => {\n const href = buildCustomerHref(item)\n if (!href) return\n router.push(href)\n }, [router])\n\n const errorMessage = error ? (error instanceof Error ? error.message : t('customers.workPlan.customerTodos.table.error.load')) : null\n const emptyStateMessage = !isLoading && !errorMessage && rows.length === 0\n ? (search ? t('customers.workPlan.customerTodos.table.state.noMatches') : t('customers.workPlan.customerTodos.table.state.empty'))\n : undefined\n\n return (\n <DataTable\n title={t('customers.workPlan.customerTodos.table.title')}\n actions={(\n <Button\n variant=\"outline\"\n onClick={() => { void handleRefresh() }}\n disabled={isFetching}\n >\n {t('customers.workPlan.customerTodos.table.actions.refresh')}\n </Button>\n )}\n columns={columns}\n data={rows}\n exporter={exportConfig}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n perspective={{ tableId: 'customers.todos.list' }}\n rowActions={(row) => {\n const customerLink = buildCustomerHref(row)\n if (!customerLink) return null\n return (\n <RowActions\n items={[\n {\n id: 'open-customer',\n label: t('customers.workPlan.customerTodos.table.actions.openCustomer'),\n href: customerLink,\n },\n ]}\n />\n )\n }}\n onRowClick={handleNavigate}\n pagination={{\n page,\n pageSize,\n total: data?.total ?? 0,\n totalPages: data?.totalPages ?? 0,\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n error={errorMessage}\n emptyState={emptyStateMessage}\n />\n )\n}\n\nexport default CustomerTodosTable\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.4.6-develop-db293c4bbe",
3
+ "version": "0.4.6-develop-6d72ec5960",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.6-develop-db293c4bbe",
210
+ "@open-mercato/shared": "0.4.6-develop-6d72ec5960",
211
211
  "@types/html-to-text": "^9.0.4",
212
212
  "@types/semver": "^7.5.8",
213
213
  "@xyflow/react": "^12.6.0",
@@ -0,0 +1,209 @@
1
+ import { z } from 'zod'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import type { EntityManager } from '@mikro-orm/postgresql'
5
+ import { CustomerTodoLink } from '../../data/entities'
6
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
+ import { createCustomersCrudOpenApi, createPagedListResponseSchema } from '../openapi'
8
+ import { decryptEntitiesWithFallbackScope } from '@open-mercato/shared/lib/encryption/subscriber'
9
+ import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
10
+ import type { EntityId } from '@open-mercato/shared/modules/entities'
11
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
12
+
13
+ const querySchema = z.object({
14
+ page: z.coerce.number().min(1).default(1),
15
+ pageSize: z.coerce.number().min(1).max(100).default(50),
16
+ search: z.string().optional(),
17
+ all: z.string().optional(),
18
+ })
19
+
20
+ export const metadata = {
21
+ GET: { requireAuth: true, requireFeatures: ['customers.view'] },
22
+ }
23
+
24
+ const TITLE_FIELDS = ['title', 'subject', 'name', 'summary', 'text'] as const
25
+ const IS_DONE_FIELDS = ['is_done', 'isDone', 'done', 'completed'] as const
26
+
27
+ function resolveTodoTitle(raw: Record<string, unknown>): string | null {
28
+ for (const key of TITLE_FIELDS) {
29
+ const value = raw[key]
30
+ if (typeof value === 'string' && value.trim()) return value.trim()
31
+ }
32
+ return null
33
+ }
34
+
35
+ function resolveTodoIsDone(raw: Record<string, unknown>): boolean | null {
36
+ for (const key of IS_DONE_FIELDS) {
37
+ const value = raw[key]
38
+ if (typeof value === 'boolean') return value
39
+ }
40
+ return null
41
+ }
42
+
43
+ async function resolveTodoSummaries(
44
+ queryEngine: QueryEngine,
45
+ links: CustomerTodoLink[],
46
+ tenantId: string | null,
47
+ orgId: string | null,
48
+ ): Promise<Map<string, { title: string | null; isDone: boolean | null }>> {
49
+ const results = new Map<string, { title: string | null; isDone: boolean | null }>()
50
+ if (!links.length || !tenantId) return results
51
+
52
+ const idsBySource = new Map<string, Set<string>>()
53
+ for (const link of links) {
54
+ if (!link.todoSource || !link.todoId) continue
55
+ if (!idsBySource.has(link.todoSource)) idsBySource.set(link.todoSource, new Set())
56
+ idsBySource.get(link.todoSource)!.add(link.todoId)
57
+ }
58
+
59
+ const requestedFields = ['id', ...TITLE_FIELDS, ...IS_DONE_FIELDS]
60
+ const organizationIds = orgId ? [orgId] : undefined
61
+
62
+ for (const [source, idSet] of idsBySource.entries()) {
63
+ const ids = Array.from(idSet)
64
+ try {
65
+ const result = await queryEngine.query<Record<string, unknown>>(source as EntityId, {
66
+ tenantId,
67
+ organizationIds,
68
+ filters: { id: { $in: ids } },
69
+ fields: requestedFields,
70
+ includeCustomFields: false,
71
+ page: { page: 1, pageSize: Math.max(ids.length, 1) },
72
+ })
73
+ for (const item of result.items ?? []) {
74
+ const raw = item as Record<string, unknown>
75
+ const todoId = typeof raw.id === 'string' ? raw.id : String(raw.id ?? '')
76
+ if (!todoId) continue
77
+ results.set(`${source}:${todoId}`, {
78
+ title: resolveTodoTitle(raw),
79
+ isDone: resolveTodoIsDone(raw),
80
+ })
81
+ }
82
+ } catch {
83
+ // non-critical: todo metadata unavailable, items fall back to null
84
+ }
85
+ }
86
+
87
+ return results
88
+ }
89
+
90
+ export async function GET(request: Request): Promise<Response> {
91
+ const auth = await getAuthFromRequest(request)
92
+ if (!auth?.sub && !auth?.isApiKey) {
93
+ return new Response(JSON.stringify({ error: 'Authentication required' }), {
94
+ status: 401,
95
+ headers: { 'Content-Type': 'application/json' },
96
+ })
97
+ }
98
+
99
+ const url = new URL(request.url)
100
+ const parsed = querySchema.safeParse({
101
+ page: url.searchParams.get('page') ?? undefined,
102
+ pageSize: url.searchParams.get('pageSize') ?? undefined,
103
+ search: url.searchParams.get('search') ?? undefined,
104
+ all: url.searchParams.get('all') ?? undefined,
105
+ })
106
+
107
+ if (!parsed.success) {
108
+ return new Response(JSON.stringify({ error: 'Invalid parameters' }), {
109
+ status: 400,
110
+ headers: { 'Content-Type': 'application/json' },
111
+ })
112
+ }
113
+
114
+ const { page, pageSize, search, all } = parsed.data
115
+ const exportAll = parseBooleanToken(all)
116
+
117
+ const container = await createRequestContainer()
118
+ const em = container.resolve('em') as EntityManager
119
+
120
+ const where: Record<string, unknown> = {
121
+ tenantId: auth.tenantId,
122
+ }
123
+ if (auth.orgId) {
124
+ where.organizationId = auth.orgId
125
+ }
126
+
127
+ if (search?.trim()) {
128
+ where.entity = { displayName: { $ilike: `%${search.trim()}%` } }
129
+ }
130
+
131
+ const [links, total] = await em.findAndCount(
132
+ CustomerTodoLink,
133
+ where,
134
+ {
135
+ populate: ['entity'],
136
+ orderBy: { createdAt: 'desc' },
137
+ ...(exportAll ? {} : {
138
+ offset: (page - 1) * pageSize,
139
+ limit: pageSize,
140
+ }),
141
+ },
142
+ )
143
+
144
+ await decryptEntitiesWithFallbackScope(links, {
145
+ em,
146
+ tenantId: auth.tenantId,
147
+ organizationId: auth.orgId ?? null,
148
+ })
149
+
150
+ const queryEngine = container.resolve('queryEngine') as QueryEngine
151
+ const todoSummaries = await resolveTodoSummaries(queryEngine, links, auth.tenantId, auth.orgId ?? null)
152
+
153
+ const effectivePage = exportAll ? 1 : page
154
+ const effectivePageSize = exportAll ? total : pageSize
155
+
156
+ const items = links.map((link) => {
157
+ const summary = todoSummaries.get(`${link.todoSource}:${link.todoId}`) ?? null
158
+ return {
159
+ id: link.id,
160
+ todoId: link.todoId,
161
+ todoSource: link.todoSource,
162
+ todoTitle: summary?.title ?? null,
163
+ todoIsDone: summary?.isDone ?? null,
164
+ todoOrganizationId: link.organizationId,
165
+ organizationId: link.organizationId,
166
+ tenantId: link.tenantId,
167
+ createdAt: link.createdAt.toISOString(),
168
+ customer: {
169
+ id: link.entity.id,
170
+ displayName: link.entity.displayName,
171
+ kind: link.entity.kind,
172
+ },
173
+ }
174
+ })
175
+
176
+ return new Response(
177
+ JSON.stringify({
178
+ items,
179
+ total,
180
+ page: effectivePage,
181
+ pageSize: effectivePageSize,
182
+ totalPages: exportAll ? 1 : Math.ceil(total / pageSize),
183
+ }),
184
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
185
+ )
186
+ }
187
+
188
+ const todoItemSchema = z.object({
189
+ id: z.string(),
190
+ todoId: z.string(),
191
+ todoSource: z.string(),
192
+ todoTitle: z.string().nullable(),
193
+ todoIsDone: z.boolean().nullable(),
194
+ todoOrganizationId: z.string().nullable(),
195
+ organizationId: z.string(),
196
+ tenantId: z.string(),
197
+ createdAt: z.string(),
198
+ customer: z.object({
199
+ id: z.string().nullable(),
200
+ displayName: z.string().nullable(),
201
+ kind: z.string().nullable(),
202
+ }),
203
+ })
204
+
205
+ export const openApi: OpenApiRouteDoc = createCustomersCrudOpenApi({
206
+ resourceName: 'CustomerTodo',
207
+ querySchema,
208
+ listResponseSchema: createPagedListResponseSchema(todoItemSchema),
209
+ })
@@ -4,11 +4,10 @@ import * as React from 'react'
4
4
  import Link from 'next/link'
5
5
  import { useRouter } from 'next/navigation'
6
6
  import type { ColumnDef } from '@tanstack/react-table'
7
- import { useQuery } from '@tanstack/react-query'
7
+ import { useQuery, keepPreviousData } from '@tanstack/react-query'
8
8
  import { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'
9
9
  import type { PreparedExport } from '@open-mercato/shared/lib/crud/exporters'
10
10
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
11
- import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
12
11
  import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
13
12
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
13
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
@@ -68,7 +67,6 @@ export function CustomerTodosTable(): React.JSX.Element {
68
67
  const [search, setSearch] = React.useState('')
69
68
  const [page, setPage] = React.useState(1)
70
69
  const [pageSize] = React.useState(50)
71
- const [filters, setFilters] = React.useState<FilterValues>({})
72
70
 
73
71
  const params = React.useMemo(() => {
74
72
  const usp = new URLSearchParams({
@@ -76,10 +74,8 @@ export function CustomerTodosTable(): React.JSX.Element {
76
74
  pageSize: String(pageSize),
77
75
  })
78
76
  if (search.trim().length > 0) usp.set('search', search.trim())
79
- const doneValue = filters.is_done
80
- if (doneValue === 'true' || doneValue === 'false') usp.set('isDone', doneValue)
81
77
  return usp.toString()
82
- }, [page, pageSize, search, filters])
78
+ }, [page, pageSize, search])
83
79
 
84
80
  const columns = React.useMemo<ColumnDef<CustomerTodoItem>[]>(() => [
85
81
  {
@@ -144,6 +140,7 @@ export function CustomerTodosTable(): React.JSX.Element {
144
140
  { errorMessage: t('customers.workPlan.customerTodos.table.error.load') },
145
141
  )
146
142
  },
143
+ placeholderData: keepPreviousData,
147
144
  })
148
145
 
149
146
  const rows = data?.items ?? []
@@ -174,33 +171,6 @@ export function CustomerTodosTable(): React.JSX.Element {
174
171
  },
175
172
  }), [rows, t, viewExportColumns])
176
173
 
177
- const filterDefs = React.useMemo<FilterDef[]>(() => [
178
- {
179
- id: 'is_done',
180
- label: t('customers.workPlan.customerTodos.table.filters.done'),
181
- type: 'select',
182
- options: [
183
- { label: t('customers.workPlan.customerTodos.table.filters.doneOption.any'), value: '' },
184
- { label: t('customers.workPlan.customerTodos.table.filters.doneOption.open'), value: 'false' },
185
- { label: t('customers.workPlan.customerTodos.table.filters.doneOption.completed'), value: 'true' },
186
- ],
187
- },
188
- ], [t])
189
-
190
- const onFiltersApply = React.useCallback((next: FilterValues) => {
191
- const nextValue = next?.is_done
192
- setFilters((prev) => {
193
- if (prev.is_done === nextValue) return prev
194
- return { is_done: nextValue }
195
- })
196
- setPage(1)
197
- }, [])
198
-
199
- const onFiltersClear = React.useCallback(() => {
200
- setFilters({})
201
- setPage(1)
202
- }, [])
203
-
204
174
  const handleRefresh = React.useCallback(async () => {
205
175
  try {
206
176
  await refetch()
@@ -218,16 +188,17 @@ export function CustomerTodosTable(): React.JSX.Element {
218
188
  }, [router])
219
189
 
220
190
  const errorMessage = error ? (error instanceof Error ? error.message : t('customers.workPlan.customerTodos.table.error.load')) : null
221
- const isEmpty = !isLoading && !errorMessage && rows.length === 0
191
+ const emptyStateMessage = !isLoading && !errorMessage && rows.length === 0
192
+ ? (search ? t('customers.workPlan.customerTodos.table.state.noMatches') : t('customers.workPlan.customerTodos.table.state.empty'))
193
+ : undefined
222
194
 
223
195
  return (
224
- <div className="space-y-4">
225
- <DataTable
226
- title={t('customers.workPlan.customerTodos.table.title')}
227
- actions={(
228
- <Button
229
- variant="outline"
230
- onClick={() => { void handleRefresh() }}
196
+ <DataTable
197
+ title={t('customers.workPlan.customerTodos.table.title')}
198
+ actions={(
199
+ <Button
200
+ variant="outline"
201
+ onClick={() => { void handleRefresh() }}
231
202
  disabled={isFetching}
232
203
  >
233
204
  {t('customers.workPlan.customerTodos.table.actions.refresh')}
@@ -242,10 +213,6 @@ export function CustomerTodosTable(): React.JSX.Element {
242
213
  setPage(1)
243
214
  }}
244
215
  perspective={{ tableId: 'customers.todos.list' }}
245
- filters={filterDefs}
246
- filterValues={filters}
247
- onFiltersApply={onFiltersApply}
248
- onFiltersClear={onFiltersClear}
249
216
  rowActions={(row) => {
250
217
  const customerLink = buildCustomerHref(row)
251
218
  if (!customerLink) return null
@@ -270,20 +237,9 @@ export function CustomerTodosTable(): React.JSX.Element {
270
237
  onPageChange: setPage,
271
238
  }}
272
239
  isLoading={isLoading}
273
- />
274
- {errorMessage ? (
275
- <div className="rounded-md border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive">
276
- {errorMessage}
277
- </div>
278
- ) : null}
279
- {isEmpty ? (
280
- <div className="py-8 text-sm text-muted-foreground">
281
- {search || filters.is_done
282
- ? t('customers.workPlan.customerTodos.table.state.noMatches')
283
- : t('customers.workPlan.customerTodos.table.state.empty')}
284
- </div>
285
- ) : null}
286
- </div>
240
+ error={errorMessage}
241
+ emptyState={emptyStateMessage}
242
+ />
287
243
  )
288
244
  }
289
245